发布 TypeScript 包时如何处理可选的对等依赖项?
Posted
技术标签:
【中文标题】发布 TypeScript 包时如何处理可选的对等依赖项?【英文标题】:How do I handle optional peer dependencies when publishing a TypeScript package? 【发布时间】:2019-06-20 21:55:34 【问题描述】:当向 npm 发布一个 TypeScript 包时,它提供了一个接受来自一个或另一个对等依赖项的输入的函数,我如何定义可选的对等依赖项?
import ExternalFoo from 'foo';
import ExternalBar from 'bar';
export const customPackage = (source: ExternalFoo | ExternalBar) =>
/* ... */
当缺少两个选项之一时,如何防止使用我的包的人出错?
【问题讨论】:
您是真的使用foo
或bar
的代码还是只需要类型信息?
我只使用类型信息
【参考方案1】:
从 Typescript 3.8 开始,您可以使用以下语法:
import type ExternalFoo from "foo";
因此,如果您只是将该库用于类型信息,您可能不必再将其列为dependency
或optionalDependency
。您可能更愿意将其保留为peerDependency
,这样如果您的用户将拥有这些依赖项,他们将使用与您的库兼容的版本。当然,添加为devDependency
也很有用。
导入将仅存在于生成的d.ts
文件上,而不存在于.js
转译代码上。但是,一个问题是,如果用户没有安装该库,则该类型将变为any
,这可能会使您自己的输入有点混乱。例如,如果foo
没有安装,你的函数就变成了
customPackage = (source: any | ExternalBar) =>
// equivalent to customPackage = (source: any) =>
对于这个特定的类型注释,它很糟糕,因为即使我安装了bar
,它也不会用于该类型检查。因此,有一种方法可以不再依赖该库,但它并不能解决编写不会崩溃的类型注释的困难类型不存在。
编辑:请查看this answer,了解如何处理缺失类型。
Reference
【讨论】:
请务必注意,如果使用代码未指定skipLibCheck
TS 编译器选项,此语法将导致错误。
更新:如果你不想强制消费者使用这个选项,你可以把// @ts-ignore
放在import type
之前。拥有foo
的消费者将看到ExternalFoo
,而没有any
的消费者将看到。 (我很尴尬:我从来没有向下滚动到最后一个答案看到这个。对不起!)【参考方案2】:
你的情况是目前 TypeScript 支持不好的情况。
我们先总结一下你的情况:
foo
和 bar
是您的可选依赖项,这意味着您希望您的消费者将其中一个与您的库一起使用。
您只使用这些库中的类型信息,这意味着您没有任何代码依赖性,也不想将它们作为dependencies
添加到您的package.json
您的customPackage
函数是公开的。
由于第 3 点,您需要在库类型中包含类型,这意味着您需要添加 foo
和 bar
作为依赖项。这与第 1 点和第 2 点相矛盾。
如果foo
和bar
的类型来自DefinitelyTyped(即来自包@types/foo
和@types/bar
),那么您可以将它们添加为dependencies
中的package.json
。这样就可以解决问题了。
如果 foo
和 bar
的类型与库本身一起分发,您必须将库包含为 dependencies
(您不想要),
或自己创建 ExternalFoo
和 ExternalBar
类型的副本。
这意味着你将不再依赖foo
和bar
。
另一种方法是仔细查看您的库,看看将foo
和bar
作为依赖项包含是否有任何危害。根据您图书馆的性质,它可能没有您想象的那么糟糕。
就个人而言,我通常会自己声明类型。 javascript 是一门动态语言。
【讨论】:
我同意声明自己的类型是正确的做法。这似乎是声明一个适合ExternalFoo
和 ExternalBar
的 interface
的好用例,这可能允许用户弄清楚他们想要提供什么(包括用于测试的模拟版本)【参考方案3】:
结合所有当前答案,这是我为当前版本的 TypeScript(截至 2021 年底)找到的最佳解决方案:
// @ts-ignore -- optional interface, will gracefully degrade to `any` if `foo` isn't installed
import type Foo from "foo";
import type Bar from "bar";
// Equates to `Bar` when `foo` isn't installed, `Foo | Bar` when it is
type Argument = any extends Foo ? Bar : (Foo | Bar);
export function customPackage(source: Argument): void
...
您可以自己尝试一下。如果安装了foo
,则导出的方法将接受Foo
或Bar
参数,如果没有,则只接受Bar
(而不是any
)。
【讨论】:
不确定这是否真的有效。我创建了一个可选择使用另一个库的库,并在peerDependencies
中声明它。当我的应用程序包含没有该依赖关系的库时,我无法构建我的应用程序,因为node_modules/my-library/some-file.d.ts
中的.d.ts
文件将生成TS2307: Cannot find module
。有什么方法可以解决这个问题而不求助于declare
:ing 外部模块或使用动态导入?
所以,node_modules/my-library/some-file.d.ts
在 import 语句上方有一个 ts-ignore
注释,但你还是得到了 TS2307
错误?我不知道如何在 cmets 中解决这个问题,但也许你可以在某个地方放一个最小的复制品?
也许我把事情搞混了,但我知道我在之前处理 .d.ts 文件时遇到了 TS2307 错误。现在,当尝试设置 MRE 时,当我的代码尝试从库中导入任何内容时,我得到 javascript require 错误 - 但这显然是因为我的库在其 index.ts
文件中有一个 export * from ...
语句,这意味着转译的 JS 将需要依次无条件地尝试要求缺少的依赖项的文件。
在使用来自可选依赖项的函数的库中定义类似乎很难处理动态导入,因为它要求它是异步的...... :(
我看到的更常用的上下文是从可选依赖项中导入接口或类“形状”,然后定义一个支持该类型参数的函数。我有一个真实世界的示例,它可以在支持这些数据的环境中采用 ArrayBuffer 或具有相同数据的 Node ReadableStream。您不必导入 ReadableStream 构造函数即可使用作为参数传递的实例,因此异步导入没有问题。【参考方案4】:
这是一个复杂的情况,但我发现可行的是在导入用户环境中可能不存在的类型之前添加一个 ts-ignore:
// @ts-ignore
import type Something from "optional"
import type SomethingElse from "required"
然后您仍然可以将包添加到peerDependencies
和peerDependenciesMeta
作为可选。
【讨论】:
以上是关于发布 TypeScript 包时如何处理可选的对等依赖项?的主要内容,如果未能解决你的问题,请参考以下文章