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@^28.0.0

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:

  1. Type it's desired Options object.
  2. Use OptionsSchema<Options> to define the options schema that matches it.
  3. Use makeAcceptsFlags<Options>()(flags) to define accepts flags.
  4. Use PayloadData<Options, typeof accepts> to define processor's Payload identity and export it.
  5. Use the Payload to register a processor.
  6. Define processor dependencies if any and export them.
  7. 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.