如何为TypeScript配置自定义全局接口(.d.ts文件)?

199

我目前正在开发一个ReactJS项目,该项目使用Webpack2和TypeScript。一切都运行得非常完美,但有一件事——我找不到一种方法将我自己编写的接口移动到单独的文件中,以便整个应用程序都可以看到它们。

为了原型设计目的,我最初在使用它们的文件中定义了接口,但最终我开始添加一些在多个类中需要的接口,这就是所有问题开始的地方。无论我对tsconfig.json做出什么更改,也无论我把文件放在哪里,我的IDE和Webpack都会抱怨找不到名称("Could not find name 'IMyInterface'")。

这是我的当前的tsconfig.json文件:

{
  "compilerOptions": {
    "baseUrl": "src",
    "outDir": "build/dist",
    "module": "commonjs",
    "target": "es5",
    "lib": [
      "es6",
      "dom"
    ],
    "typeRoots": [
      "./node_modules/@types",
      "./typings"
    ],
    "sourceMap": true,
    "allowJs": true,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDir": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": false,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true
  },
  "exclude": [
    "node_modules",
    "build",
    "scripts",
    "acceptance-tests",
    "webpack",
    "jest",
    "src/setupTests.ts"
  ],
  "types": [
    "typePatches"
  ]
}

如您所见,我的 tsconfig.json 文件位于项目目录的根目录下,所有源代码在 ./src 中,我将自定义的 .d.ts 文件放置在 ./typings 中,并在 typeRoots 中包含它。

我已经使用 TypeScript 2.1.6 和 2.2.0 进行了测试,但都不起作用。

让所有内容都正常工作的一种方法是将我的 typings 目录移动到 src 中,然后使用 import {IMyInterface} from 'typings/blah',但我觉得这不是必须的。我希望这些接口可以在整个应用程序中“神奇地”自动使用。

以下是一个示例的 app.d.ts 文件:

interface IAppStateProps {}
interface IAppDispatchProps {}
interface IAppProps extends IAppStateProps, IAppDispatchProps {}

我需要将它们export还是declare?我希望不需要将它们包装在命名空间中!更新(2020年10月):鉴于这个问题仍然出奇地受欢迎,我想更详细地解释一下解决方案。首先,可能会让人困惑的是,在我的问题结尾处给出的接口示例实际上没有任何export关键字,即使我几乎可以确定在提问时我的文件中有这些关键字。我相信我没有在问题中包含它们,认为它们存在或不存在并没有任何区别。事实证明这是不正确的,export关键字正是能够使你只需“使用”接口而不必显式地import它们的关键所在。因此,在TypeScript中的工作方式如下:如果你想要一个接口/类型,你可以简单地使用它而不必在消费模块中导入它,那么该接口必须驻留在一个.ts或理想情况下是一个.d.ts文件中没有任何导入或导出存在于同一文件中。这是至关重要的,因为一旦你从同一文件中导出至少一个东西,它就成为了一个模块,而该模块中的所有内容都必须由消费者随后导入。举个例子,假设你想要一个名为Dictionary的类型,你想能够在不导入的情况下使用它。声明它的方法如下:
// types.d.ts
interface Dictionary {}
interface Foo {}
interface Bar {}

使用它很简单,只需执行以下操作:

// consumer.ts
const dict: Dictionary = {};

然而,如果该文件中的任何接口/类型被导出,它将不再起作用,例如:

// types.d.ts
interface Dictionary {}
interface Foo {}
export interface Bar {}

它也无法工作,如果该文件中存在导入项:
// types.d.ts
import { OtherType } from 'other-library';

interface Dictionary {}
interface Foo extends OtherType {}
interface Bar {}

如果是这种情况,要使用Dictionary类型的唯一方法就是将其导出并在消费者中导入:
// types.d.ts
export interface Dictionary {}
interface Foo {}
export interface Bar {}

// consumer.ts
import { Dictionary } from './types';

const dict: Dictionary = {};

--isolatedModules
在TypeScript中使用 --isolatedModules 模块标志时需要注意一个额外的问题,重要的是,当使用Create React App时,默认情况下启用此标志(无法禁用) - .ts 文件必须至少导出一个内容,否则将会收到"提供'--isolatedModules'标志时,所有文件都必须是模块。"错误。这意味着将 Dictionary 接口放在没有导出关键字的 types.ts 文件中将不起作用。它必须是从 .ts 文件中导出,或者是在 .d.ts 文件中没有导出的声明。
// types.d.ts
interface Dictionary {}        // works
export interface Dictionary {} // works

// types.ts
interface Dictionary {}        // doesn't work with --isolatedModules enabled
export interface Dictionary {} // works

N.B.
正如@dtabuenc在他的答案中提到的,环境模块(.d.ts文件)是不鼓励使用的,我的更正不应被视为建议。这只是一种尝试,解释TypeScript中普通模块和环境模块的工作原理。

2个回答

367
"不建议使用魔术般的可用接口或全局类型,这应该主要留给旧版本。同时,您不应该为您编写的代码使用环境声明文件(例如 d.ts 文件)。这些文件是为外部非 TypeScript 代码提供支持的(基本上是将 TypeScript 类型填充到 JS 代码中,以便更好地将其与 JavaScript 集成)。

对于您编写的代码,应该使用普通的 .ts 文件来定义接口和类型。

虽然不鼓励使用全局类型,但解决问题的答案是 TypeScript 中有两种类型的 .ts 文件,这些文件分别被称为“脚本”和“模块”。

在“脚本”中的任何内容都将成为全局内容。因此,如果您在脚本中定义接口,则它将在整个应用程序范围内全局可用(只要通过 ///<reference path=""> 标签或通过 files:[]includes:[] 或在您的 tsconfig.json 中使用默认的 **/*.ts 编译选项包含了该脚本)。

另一种文件类型是“模块”,在“模块”中的任何内容都将仅限于该模块。如果从模块中导出任何内容,则在其他模块中导入它们时将可用。

什么会使一个 .ts 文件成为“脚本”或“模块”呢?好的.... 如果您在文件中使用了 import/export,那么该文件就成为了“模块”。如果没有 import/export 语句,则为全局脚本。

我猜您无意中在声明中使用了 importexport,并将其转换为了模块,从而让所有接口在该模块内变为了私有。如果您希望它们是全局的,则必须确保文件内没有使用 import/export 语句。

"

5
你说得没错!我的问题就像你所建议的那样,是我从其他文件中导入类/接口到我的声明文件中。一旦我删除了这些导入并且只留下干净的“interface Foo {}”声明,一切都顺利解决了。至于全局变量 - 我完全明白为什么让声明自动可用是个不好的做法,并考虑切换到自定义模块(实际上尝试过declare module MyApp { export interface Foo {} },并且可以通过import { {Foo} from 'MyApp'}成功导入),但目前魔法对我来说可以工作 :) - Andris
44
哇靠,兄弟。我刚刚浪费了一整天的时间,根本不知道自己在干什么。非常感谢你。如果这个答案还没有被列入官方文档,请把它加进去。 - Sam R.
1
我将一个项目暂停了一周,思考它为何不能运行。我私下学习了些本来只能坚持三天的内容。非常感谢 dtabuenc - Salathiel Genèse
5
你能支持一下全局类型已被弃用的说法吗?从官方文档上看,我没有这样的印象。 - Tomek
5
“Magically available interfaces”是为基于JS的领域特定语言提供基本IDE支持的两种方式之一,另外一种方式则是“字面上难度要高出数百甚至数千倍”:为所有IDE编写插件。 - Szczepan Hołyszewski
显示剩余7条评论

10
术语是“全局”接口,而不是“神奇可用”接口。
在某些.d.ts文件中,我扩展了全局Window接口,例如:
export {};

declare global {
    interface Window {
        // Below just informs IDE and/or TS-compiler (it's set in `.js` file).
        myGlobalVariable: any;
    }
}

使用相同的技巧,我们可以创建自定义全局接口(而不是扩展现有接口)。

示例

import React from 'react';

declare global {
    interface MyGlobalInterface {
        myComponentField: React.Component;
    }
}

请注意,由于 .d.ts 文件不会产生任何输出,因此我们可以导入任何模块(当然,只要该模块已经被 package.json 安装了)。
另外,上面的示例不需要 export {}; 行, 因为它确实有一个 import, 我的意思是,规则是至少有一个 import 或一个 export(在撰写时)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接