typescript - 如何为具有大量私有类的功能模块组织 Typescript 类型定义?
问题描述
我正在为我不拥有的名为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;
它导出一个返回对象的函数,该对象的每个成员都是一个类。(到目前为止,我在 lib 中遇到的每个文件都使用默认导出。)我从模块函数模板开始,但我很难在我的一些指导原则之间找到和谐:
- 类型定义应该促进使用这个库的最佳实践。我的意思是,我不会费心为标记为
@private
或不打算/不建议外部使用的方法创建定义。根据库文档,getFessonia
是该库的唯一公共接口。虽然没有什么可以阻止开发人员FFmpegCommand
直接导入,但不应该(因为,例如,本应设置的配置getFessonia
不会被设置,并且可能会导致错误)。 - 类型定义应该很有用。下游开发人员应该能够将类型分配给他们的变量 à la:
import getFessonia from '@tedconf/fessonia';
// note the type assignment
const config: getFessonia.ConfigOpts = {
debug: true,
};
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia(config);
- 声明文件的布局应反映库的布局。根据官方推荐。
到目前为止,我采用的方法是.d.ts
为每个.js
文件创建有用的类型定义所需的文件,然后将它们导入index.d.ts
并根据需要在命名空间中重新导出getFessonia
。例如,为了为opts
参数提供类型定义,我需要 read lib/util/config
,它有一个默认的 export 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>;
}
我认为我可能走错路的原因:
- 我认为我不需要对 function 进行定义
getConfig
,尤其是因为我不想推广它的直接使用。lib/util/config
有默认导出是否重要?我应该Config
直接导出接口并从中重新导出index.d.ts
吗?或者也许我会删除函数定义并将Config
接口保留在命名空间下;那样,getConfig
将来应该成为公共功能,我可以添加该功能的定义。 - 在命名空间下重新导出
getFessonia
是乏味的,而且不是特别优雅。 - 我最终可能会在
getFessonia
. 例如,构造函数FFmpegOutput
接受一个参数,该参数实际上只是内部类的参数映射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
参数 to 的定义和 to的形状是兄弟姐妹的定义不是很直观FFmpegOutput
。FFmpeg
出于组织/命名冲突避免的原因,我正在构建命名空间。
你做到了最后!感谢您阅读本文。虽然我怀疑没有一个“正确”的答案,但我期待阅读其他人采用的方法,并且很高兴有人指出我可以通过示例学习的文章或相关代码存储库。谢谢!
解决方案
@alex-wayne的评论帮助我重置了大脑。谢谢你。
出于某种原因,我正在编写类型定义,就好像库对默认导出的使用意味着我也无法从我的.d.ts
文件中导出其他内容。可能是睡眠不足!
无论如何,除了默认导出函数之外,getFessonia
我最终导出了一个接口Fessonia
来描述返回值以及同名的命名空间(更多关于 TypeScript 的组合行为)来为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
对象一部分的类,我的一般方法是为每个类创建一个类型定义(省略私有成员)并将其导出。如果类函数具有复杂类型的参数,我会为它们创建定义并将它们导出到与类同名的命名空间中,例如:
// 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
命名空间)。它是可读的。
我第一次输入这个库可以在 GitHub 上找到。
感谢所有评论并让我有不同想法的人。
推荐阅读
- python - TensorFlow - 从 MNIST 识别特定数字(例如 7)
- javascript - 如何在返回的表格中为每一行添加换行符?
- laravel - 无法使用作曲家安装护照
- java - 在 JBoss DB 源配置中获取“ORA-12514,TNS:listener 目前不知道连接描述符中请求的服务”
- c - 为什么取消引用后指针会打印不同的值?
- xamarin - 我的 Xamarin Form EditText 事件 FocusChange 不会触发
- reactjs - 让 SSR 与 React Context API 一起工作
- shell - 使用管道时奇怪的二进制符号
- swift - Swift 4-如何实现 UIViews 之间的滑动以重新创建 Spotify 歌曲选择
- android - AsyncTask 和 Volley 库的问题