Typing
A guide on how to achieve type safety when writing plugins in TypeScript.
If you have ideas how to improve this, please file a new, or contribute to already existing issues.
#Install
This happens automatically if you're using the boilerplate generator included in the app, but in case you're doing it manually, you need to install a package that provides the plugin types and helpers:
npm install --save-dev @drovp/types
This package also installs @types/node
.
If you need types for electron's renderer process (to be used in operation preparator for example), I'm afraid you'll have to install electron
into your devDependencies
. Please, file an issue if you know of a less wasteful way how to get them, but as far as I know the electron types are not isolated in any standalone package.
npm install --save-dev electron@^32.1.2
#Usage
Intended to be consumed via the import
keyword.
import {PayloadData, OptionsSchema, makeAcceptsFlags} from '@drovp/types';
All available types are defined in drovp/types/index.d.ts
.
#Processor
To properly type a processor we need to:
- Type it's desired
Options
object. - Use
OptionsSchema<Options>
to define the options schema that matches it. - Use
makeAcceptsFlags<Options>()(flags)
to define accepts flags. - Use
PayloadData<Options, typeof accepts>
to define processor'sPayload
identity and export it. - Use the
Payload
to register a processor. - Define processor dependencies if any and export them.
- Create a processor for the above
Payload
.
The most important concern for the implementer is to ensure that Options
interface and options schema used to generate it are in sync.
#main.ts
(plugin)
import {App, PayloadData, OptionsSchema, makeAcceptsFlags} from '@drovp/types';
type Options = {
bulk: boolean;
allowFiles: boolean;
fileTypes: string[];
};
const schema: OptionsSchema<Options> = [
{name: 'bulk', type: 'boolean'},
{name: 'allowFiles', type: 'boolean'},
{
name: 'fileTypes',
type: 'select',
default: [],
options: ['jpg', 'png', 'gif'],
isHidden: (value, options) => options.allowFiles,
},
];
const accepts = makeAcceptsFlags<Options>()({
files: (item, options) => options.allowFiles && options.fileTypes.includes(item.type),
});
export type Payload = PayloadData<Options, typeof config>;
// Omit if no dependencies
export interface Dependencies {
ffmpeg: string;
}
export default (app: App) => {
app.registerProcessor<Payload>('foo', {
main: 'dist/processor.js',
dependencies: ['@drovp/ffmpeg:ffmpeg'],
accepts: accepts,
options: schema,
bulk: (items, options) => options.bulk,
});
};
#processor.ts
import type {ProcessorUtils} from '@drovp/types';
import type {Payload, Dependencies} from './';
export default async (payload: Payload, utils: ProcessorUtils<Dependencies>) => {
console.log(payload); // ItemFile(s), profile options, and extra data if any
console.log(utils.dependencies.ffmpeg); // path to ffmpeg binary
};
The Payload
and Dependencies
are constructed and imported from the main plugin file above.
Payload
, if typed properly, holds the exact shape of the payload that is going to be sent from the app to the processor.
Dependencies
need to be defined manually depending on what your processor depends on, and what those dependencies export on their payloads. See dependencies
documentation for details.
#Dependency
Typing dependency is pretty straight forward. Either pass the config directly to the registerDependency()
:
import {App} from '@drovp/types';
export default (app: App) => {
app.registerDependency('foo', {
load: async (utils) => 'payload',
install: async (utils) => {
/* install logic */
},
});
};
Or define functions separately:
import {App, LoadUtils, InstallUtils} from '@drovp/types';
async function load(utils: LoadUtils) {
// load logic
return 'payload';
}
async function install(utils: InstallUtils) {
// install logic
}
export default (app: App) => {
app.registerDependency('foo', {load, install});
};
#Helpers
These are interface constructors, and passthrough constrained identity functions. The purpose is to provide a way to define interfaces without loosing intellisense, error reporting, and the resulting interface identity so that it can be used to construct further higher order interfaces.
#PayloadData
PayloadData<Options?, AcceptsFlags?, Extra?>;
Constructor for processor payload object. It accepts optional Options
, AcceptsFlags
, and an Extra
object in case you're adding something more to the final payload inside a processor preparator.
Example
type Options = {foo: boolean; bar: string};
const schema: OptionsSchema<Options> = [/*...*/];
const accepts = makeAcceptsFlags<Options>()({files: true});
type ExtraProps = {
someProps: boolean; // props attached by operation preparator
};
export type Payload = PayloadData<Options, typeof accepts, ExtraProps>;
// Payload now looks like:
interface Payload {
readonly id: string;
options: Options;
inputs: ItemFile[];
input: ItemFile;
extraProp: string;
}
#makeOptionsSchema()
makeOptionsSchema<Options>()(schema: OptionsSchema): typeof schema;
Helps defining options schema for the Options
interface.
Note the extra ()
. This is necessary for generic inference to work properly.
Example
type Options = {foo: boolean; bar: string};
const schema = makeOptionsSchema<Options>()([
{name: 'foo', type: 'boolean'},
{name: 'bar', type: 'string', isHidden: (value, options) => options.foo},
]);
This is currently not really useful, you can just do:
type Options = {foo: boolean; bar: string};
const schema: OptionsSchema<Options> = [/*...*/];
#makeAcceptsFlags()
makeAcceptsFlags<Options>()(flags: AcceptsFlags): typeof flags;
Helps defining accepts flags for the Options
interface.
Note the extra ()
. This is necessary for generic inference to work properly.
Example
type Options = {foo: boolean; bar: string};
const accepts = makeAcceptsFlags<Options>({
files: (item, options) => item.type === options.bar,
});
#makeProcessorConfig()
makeProcessorConfig<Payload>()(config: ProcessorConfig): typeof config;
Helps defining ProcessorConfig
for the specified Payload
. Payload
itself is a result of PayloadData
below.
Note the extra ()
. This is necessary for generic inference to work properly.
Example
type Options = {foo: boolean; bar: string};
const schema = makeOptionsSchema<Options>()([
{name: 'foo', type: 'boolean'},
{name: 'bar', type: 'string', isHidden: (value, options) => options.foo},
]);
const accepts = makeAcceptsFlags<Options>()({
files: (item, options) => item.type === options.bar,
});
export type Payload = PayloadData<Options, typeof accepts>;
const config = makeProcessorConfig<Payload>()({
main: 'processor.js',
accepts: accepts,
options: schema,
});
export default (app: App) => app.registerProcessor<Payload>('foo', config);
Note that if processor config is constructed for a specific Payload
, you need to also pass it to the registerProcessor<Payload>()
generic parameter.
#makeDependencyConfig()
makeDependencyConfig(config: ProcessorConfig): typeof config;
Helps defining DependencyConfig
. There is currently no need to define it separately from app.registerDependency()
, so this helper is just for completeness.