I'm writing type definitions for a library I don't own called fessonia. I have some experience doing this, but this library is organized differently than others I've worked with, and I'm not sure how to approach it.
This library's index.js
is small:
const getFessonia = (opts = {}) => {
require('./lib/util/config')(opts);
const Fessonia = {
FFmpegCommand: require('./lib/ffmpeg_command'),
FFmpegInput: require('./lib/ffmpeg_input'),
FFmpegOutput: require('./lib/ffmpeg_output'),
FilterNode: require('./lib/filter_node'),
FilterChain: require('./lib/filter_chain')
};
return Fessonia;
}
module.exports = getFessonia;
It exports a function which returns an object, each member of which is a class. (Every file I've encountered in the lib so far uses default exports.) I've started with the module function template, but I'm struggling to find harmony among some of my guiding principles:
- The type definitions should promote best practices for using this library. What I mean by this is that I will not bother to create definitions for methods that are flagged as
@private
or which are otherwise not intended/recommended for external use. Per the library documentation, getFessonia
is the only public interface to this library. While there's nothing stopping a developer from importing FFmpegCommand
directly, one shouldn't (because, for example, the config that would have been set in getFessonia
won't have been set, and errors will likely result).
- The type definitions should be useful. Downstream developers should be able to assign types to their variables à la:
import getFessonia from '@tedconf/fessonia';
// note the type assignment
const config: getFessonia.ConfigOpts = {
debug: true,
};
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia(config);
So far the approach I've taken is to create .d.ts
files for each .js
file required to make useful type definitions, then import those into index.d.ts
and re-export as needed in the getFessonia
namespace. For example, in order to provide a type definition for the opts
argument, I needed to read lib/util/config
, which has a default export getConfig
. Its type file ends up looking something like this:
import getLogger from './logger';
export = getConfig;
/**
* Get the config object, optionally updated with new options
*/
declare function getConfig(options?: Partial<getConfig.Config>): getConfig.Config;
declare namespace getConfig {
export interface Config {
ffmpeg_bin: string;
ffprobe_bin: string;
debug: boolean;
log_warnings: boolean;
logger: getLogger.Logger;
}
}
... and I use it in index.d.ts
like this:
import getConfig from './lib/util/config';
export = getFessonia;
/**
* Main function interface to the library. Returns object of classes when called.
*/
declare function getFessonia(opts?: Partial<getFessonia.ConfigOpts>): getFessonia.Fessonia;
declare namespace getFessonia {
export interface Fessonia {
// TODO
FFmpegCommand: any;
FFmpegInput: any;
FFmpegOutput: any;
FilterNode: any;
FilterChain: any;
}
// note I've just aliased and re-exported this
export type ConfigOpts = Partial<getConfig.Config>;
}
Reasons I think I might be headed down the wrong path:
- I don't think I need a definition for the function
getConfig
, especially since I don't want to promote its direct usage. Does it matter that lib/util/config
has a default export? Should I just export the Config
interface directly and re-export that from index.d.ts
? Or maybe I'll delete the function definition and keep the Config
interface under the namespace; that way, should getConfig
become a public function in the future, I can just add the definition for the function.
- Re-exporting under the
getFessonia
namespace is tedious and not especially elegant.
- I could end up with a lot of nesting (and aliasing) under
getFessonia
. For example, the constructor for FFmpegOutput
takes an argument which is really just a map of arguments for an internal class FFmpegOption
, so downstream code could maybe end up looking something like:
import getFessonia from '@tedconf/fessonia';
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia();
// note the deep nesting
const outputOptions: getFessonia.FFmpeg.Output.Options = { /* some stuff */ };
const output = new FFmpegOutput('some/path', outputOptions);
- It's not very intuitive for the defintion of the argument to
getFessonia
and the shape of FFmpegOutput
to be siblings.
- I'm making up the
FFmpeg
namespace for organizational/naming-conflict-avoidance reasons.
You made it to the end! Thanks for reading this far. While I suspect there isn't one "right" answer, I look forward to reading about approaches others have taken, and I'm happy to be pointed to articles or relevant code repositories where I might learn by example. Thanks!