如何为具有大量私有 类 的功能模块组织 Typescript 类型定义?

How to organize Typescript type definitions for a function module with lots of private classes?

我正在为我不拥有的名为 fessonia 的库编写类型定义。我在这方面有一些经验,但这个库的组织方式与我合作过的其他库不同,我不确定如何处理它。

这个图书馆的 index.js 很小:

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;

它导出一个函数,其中returns一个对象,每个成员都是一个class。 (到目前为止,我在 lib 中遇到的每个文件都使用默认导出。)我从 module function template 开始,但我正在努力寻找一些指导原则之间的和谐:

import getFessonia from '@tedconf/fessonia';
// note the type assignment
const config: getFessonia.ConfigOpts = {
    debug: true,
};
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia(config);

到目前为止,我采用的方法是为创建有用类型定义所需的每个 .js 文件创建 .d.ts 文件,然后将它们导入 index.d.ts 并重新导出根据 getFessonia 命名空间中的需要。例如,为了为 opts 参数提供类型定义,我需要读取 lib/util/config,它具有默认导出 getConfig。它的类型文件最终看起来像这样:

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;
    }
}

... 我在 index.d.ts 中这样使用它:

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>;
}

我认为我可能走错路的原因:

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);

您已经走到最后了!感谢您阅读到这里。虽然我怀疑没有一个“正确”的答案,但我期待阅读其他人采用的方法,并且很高兴有人指出我可以通过示例学习的文章或相关代码存储库。谢谢!

@alex-wayne 的评论帮助我重新启动了大脑。谢谢。

出于某种原因,我在编写类型定义时就好像库对默认导出的使用意味着我无法从我的 .d.ts 文件中导出其他内容。可能是睡不够吧!

无论如何,除了默认导出函数 getFessonia 之外,我最终导出了一个接口 Fessonia 来描述 return 值以及同名的命名空间( more on TypeScript's combining behavior) 为 getFessonia 的选项以及库提供的各种其他实体提供类型。 index.d.ts 最后看起来像:

import { FessoniaConfig } from './lib/util/config';
import FFmpegCommand from './lib/ffmpeg_command';
import FFmpegInput from './lib/ffmpeg_input';
import FFmpegOutput from './lib/ffmpeg_output';
import FilterChain from './lib/filter_chain';
import FilterNode from './lib/filter_node';

/** Main function interface to the library. Returns object of classes when called. */
export default function getFessonia(opts?: Partial<Fessonia.ConfigOpts>): Fessonia;

export interface Fessonia {
  FFmpegCommand: typeof FFmpegCommand;
  FFmpegInput: typeof FFmpegInput;
  FFmpegOutput: typeof FFmpegOutput;
  FilterChain: typeof FilterChain;
  FilterNode: typeof FilterNode;
}

// re-export only types (i.e., not constructors) to prevent direct instantiation
import type FFmpegCommandType from './lib/ffmpeg_command';
import type FFmpegError from './lib/ffmpeg_error';
import type FFmpegInputType from './lib/ffmpeg_input';
import type FFmpegOutputType from './lib/ffmpeg_output';
import type FilterNodeType from './lib/filter_node';
import type FilterChainType from './lib/filter_chain';
export namespace Fessonia {
    export type ConfigOpts = Partial<FessoniaConfig>;

    export {
      FFmpegCommandType as FFmpegCommand,
      FFmpegError,
      FFmpegInputType as FFmpegInput,
      FFmpegOutputType as FFmpegOutput,
      FilterChainType as FilterChain,
      FilterNodeType as FilterNode,
    };
}

对于属于 Fessonia 对象的 classes,我的一般方法是为每个对象创建一个类型定义(省略私有成员)并将其导出。如果 class 函数具有复杂类型的参数,我会为它们创建定义并将它们导出到与 class 同名的命名空间中,例如:

// abridged version of types/lib/ffmpeg_input.d.ts
export default FFmpegInput;

declare class FFmpegInput {
    constructor(url: FFmpegInput.UrlParam, options?: FFmpegInput.Options);
}

declare namespace FFmpegInput {
    export type Options = Map<string, FFmpegOption.OptionValue> | { [key: string]: FFmpegOption.OptionValue };
    export type UrlParam = string | FilterNode | FilterChain | FilterGraph;
}

由于重新导出了index.d.ts底部的类型,因此下游代码成为可能:

import getFessonia, { Fessonia } from '@tedconf/fessonia';

const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia();
// note the type assignment
const outputOptions: Fessonia.FFmpegOutput.Options = { /* some stuff */ };
const output = new FFmpegOutput('some/path', outputOptions);
const cmd = new FFmpegCommand(commandOpts);

虽然这与我最初的想法并没有太大的不同,但确实感觉像是一种改进。我不必发明太多新的组织结构;类型名称与代码库的结构一致(添加了 Fessonia 命名空间)。而且它是可读的。

我第一次输入这个库是 available on GitHub

感谢所有发表评论并让我有不同想法的人。