如何为具有大量私有 类 的功能模块组织 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 开始,但我正在努力寻找一些指导原则之间的和谐:
- 类型定义应促进使用此库的最佳实践。我的意思是我不会费心为标记为 [=17 的方法创建定义=] 或者不是 intended/recommended 供外部使用。根据库文档,
getFessonia
是该库的唯一 public 接口。虽然没有什么可以阻止开发人员直接导入 FFmpegCommand
,但不应该(因为,例如,本应在 getFessonia
中设置的配置不会被设置,并且可能会导致错误).
- 类型定义应该很有用。下游开发人员应该能够将类型分配给他们的变量 à la:
import getFessonia from '@tedconf/fessonia';
// note the type assignment
const config: getFessonia.ConfigOpts = {
debug: true,
};
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia(config);
- 声明文件的布局应反映库的布局。 根据 official recommendation。
到目前为止,我采用的方法是为创建有用类型定义所需的每个 .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>;
}
我认为我可能走错路的原因:
- 我认为我不需要函数
getConfig
的定义,特别是因为我不想推广它的直接用法。 lib/util/config
具有默认导出是否重要?我应该直接导出 Config
接口并从 index.d.ts
重新导出吗?或者,也许我会删除函数定义并将 Config
接口保留在命名空间下;这样,以后如果getConfig
变成了public函数,我只要加上函数的定义就可以了。
- 在
getFessonia
命名空间下重新导出是乏味的,而且不是特别优雅。
- 我最终可能会在
getFessonia
下进行大量嵌套(和别名)。例如,FFmpegOutput
的构造函数接受一个参数,它实际上只是内部 class FFmpegOption
的参数映射,因此下游代码最终可能看起来像:
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);
- 把
getFessonia
的参数和FFmpegOutput
的形状定义为同级不是很直观
- 出于 organizational/naming-conflict-avoidance 个原因,我正在构建
FFmpeg
命名空间。
您已经走到最后了!感谢您阅读到这里。虽然我怀疑没有一个“正确”的答案,但我期待阅读其他人采用的方法,并且很高兴有人指出我可以通过示例学习的文章或相关代码存储库。谢谢!
@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。
感谢所有发表评论并让我有不同想法的人。
我正在为我不拥有的名为 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 开始,但我正在努力寻找一些指导原则之间的和谐:
- 类型定义应促进使用此库的最佳实践。我的意思是我不会费心为标记为 [=17 的方法创建定义=] 或者不是 intended/recommended 供外部使用。根据库文档,
getFessonia
是该库的唯一 public 接口。虽然没有什么可以阻止开发人员直接导入FFmpegCommand
,但不应该(因为,例如,本应在getFessonia
中设置的配置不会被设置,并且可能会导致错误). - 类型定义应该很有用。下游开发人员应该能够将类型分配给他们的变量 à la:
import getFessonia from '@tedconf/fessonia';
// note the type assignment
const config: getFessonia.ConfigOpts = {
debug: true,
};
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia(config);
- 声明文件的布局应反映库的布局。 根据 official recommendation。
到目前为止,我采用的方法是为创建有用类型定义所需的每个 .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>;
}
我认为我可能走错路的原因:
- 我认为我不需要函数
getConfig
的定义,特别是因为我不想推广它的直接用法。lib/util/config
具有默认导出是否重要?我应该直接导出Config
接口并从index.d.ts
重新导出吗?或者,也许我会删除函数定义并将Config
接口保留在命名空间下;这样,以后如果getConfig
变成了public函数,我只要加上函数的定义就可以了。 - 在
getFessonia
命名空间下重新导出是乏味的,而且不是特别优雅。 - 我最终可能会在
getFessonia
下进行大量嵌套(和别名)。例如,FFmpegOutput
的构造函数接受一个参数,它实际上只是内部 classFFmpegOption
的参数映射,因此下游代码最终可能看起来像:
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);
- 把
getFessonia
的参数和FFmpegOutput
的形状定义为同级不是很直观 - 出于 organizational/naming-conflict-avoidance 个原因,我正在构建
FFmpeg
命名空间。
您已经走到最后了!感谢您阅读到这里。虽然我怀疑没有一个“正确”的答案,但我期待阅读其他人采用的方法,并且很高兴有人指出我可以通过示例学习的文章或相关代码存储库。谢谢!
@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。
感谢所有发表评论并让我有不同想法的人。