如果库的实现未与TS项目集成,如何使用d.ts文件导出的const类型?

时间:2019-04-21 00:37:54

标签: javascript typescript definitelytyped

DefinitelyTyped具有许多库的类型定义,但是当Javascript实现与Typescript分开时,例如当库通过a分配给窗口的属性时,常常我找不到一种使用它们的好方法>

<script src="https://example.com/library.js">

标签,当我管理的JS包位于另一个单独的脚本中时。 (即使将包括库的所有内容都捆绑在一起是标准且可靠的方法,但出于问题的考虑,我不能选择将库导入到我的TS项目中。)例如,说我为名为myLib的库找到了一个漂亮的定义文件:

// my-lib.d.ts
export const doThing1: () => number;
export const doThing2: () => string;
export const version: string;
export interface AnInterface {
  foo: string;
}
export as namespace myLib;

在JS中,我可以通过调用window.myLib.doThing1()window.myLib.doThing2()来使用myLib。如何导入整个window.myLib对象的形状,以便可以将其声明为window的属性?我看到我可以导入导出的接口,例如:

// index.ts
import { AnInterface } from './my-lib';
const something: AnInterface = { foo: 'foo' };
console.log(something.foo);

这可行,但是我想访问实际库对象的形状及其属性值(函数和字符串等),而不仅仅是接口。如果我愿意

import * as myLib from './my-lib';

然后myLib标识符成为一个命名空间,我可以从中引用导出的接口,但是就像上面一样,我仍然无法从{访问export constexport function形状{1}}。 (而且,当然,尝试使用导入的名称空间声明库对象是行不通的:my-lib.d.ts即使这样做,也不一定安全,因为为浏览器打包的库可能会的结构与库的Node导出对象略有不同)

如果我手动将Cannot use namespace 'myLib' as a type.的一部分复制并粘贴到自己的脚本中,则可以一起破解一些有效的东西:

d.ts

但这是一个混乱,耗时的过程,并且肯定不是进行此类操作的正确方法。当我遇到这种情况时,我会喜欢做一些简短而优雅的事情,例如:

// index.ts
declare global {
  interface Window {
    myLib: {
      doThing1: () => number;
      doThing2: () => string;
      version: string;
    };
  }
}

某些定义文件包括库对象的接口,例如jQuery,它的作用是:

// index.ts
import myLibObjectInterface from './my-lib.d.ts'; // this line is not correct
declare global {
  interface Window {
    myLib: myLibObjectInterface
  }
}

然后一切都很好-我只能使用// index.d.ts /// <reference path="JQuery.d.ts" /> // jQuery.d.ts interface JQuery<TElement = HTMLElement> extends Iterable<TElement> { // lots and lots of definitions ,但是许多最初不是为浏览器使用而创建的库没有提供这样的接口。

如前所述,最好的解决方案是将库的实现与TS项目集成在一起,允许interface Window { $: jQuery }都使用库及其类型,而不必大惊小怪,但这是不可能的。 ,我还有什么 good 选项吗?我可以检查真实库对象上的属性,并向包含所有此类属性及其类型的定义文件添加接口,但必须修改DT接受并被其他人使用的半规范源定义文件错误。我希望能够导入定义文件导出的形状,并在不修改原始文件的情况下从它们创建一个接口,但这可能是不可能的。

是否有更优雅的解决方案,或者碰巧遇到的定义文件根本不适合我的目标,因此必须对其进行修改?

2 个答案:

答案 0 :(得分:8)

如果模块具有export as namespace myLib,则模块已经将库导出为全局对象。因此,您可以将库用作:

let a:myLib.AnInterface;
let b =  myLib.doThing1();

只要您正在使用该库的文件本身不是模块本身(即,它不包含import,也不包含export语句),这就是事实。

export {} // module now
let a:myLib.AnInterface; // Types are still ok without the import
let b =  myLib.doThing1(); // Expressions are not ok, ERR: 'myLib' refers to a UMD global, but the current file is a module. Consider adding an import instead.ts(2686)

您可以使用导入类型(相信2.9中添加了),将与图书馆类型相同的属性添加到Window

// myLibGlobal.d.ts
// must not be a module, must not contain import/ export 
interface Window {
    myLib: typeof import('./myLib') // lib name here
}


//usage.ts
export {} // module
let a:myLib.AnInterface; // Types are still ok without the import (if we have the export as namespace
let b =  window.myLib.doThing1(); // acces through window ok now

修改

显然,Typescript团队实际上已经在为这个问题进行某些工作。如您在此PR中所读,打字稿的下一个版本将包含一个allowUmdGlobalAccess标志。此标志将允许从模块访问UMD模块全局变量。将此标志设置为true时,此代码将有效:

export {} // module now
let a:myLib.AnInterface; // Types are still ok without the import
let b =  myLib.doThing1(); // ok, on typescript@3.5.0-dev.20190425

这意味着您无需使用窗口就可以访问模块导出。如果全局导出与我期望的浏览器兼容,那么它将起作用。

答案 1 :(得分:3)

您在做什么

  

当库将自身分配给窗口的属性时

这称为 UMD包。这些是通过在文档的<script />标签内添加链接而消耗的,它们将自身附加到全局范围。

不必以这种方式使用UMD软件包-使用import(或require)语句也可以将它们作为模块使用。

TypeScript支持这两种用法。

如何键入UMD软件包

declare namespace Foo {
  export const bar: string;
  export type MeaningOfLife = number;
}

export as namespace Foo;
export = Foo;

此定义告诉TypeScript:

  • 如果使用文件是 script ,则全局范围中存在一个名为bar的变量
  • 如果使用方文件是 module ,则可以使用命名的导入来导入bar,也可以使用通配符(*)导入来导入整个名称空间。

脚本模块有什么区别?

脚本将是HTML文档中<script />标记内运行的一段JavaScript。可以内联它或从文件中加载它。这就是浏览器始终使用JavaScript的方式。

模块是一个JavaScript(TypeScript)文件,具有至少一个importexport语句。它们是ECMAScript标准的一部分,并非到处都受支持。通常,您在项目中创建 modules ,然后让诸如Webpack的捆绑器为您的应用程序创建要使用的捆绑器。

使用UMD软件包

通过访问全局Foo命名空间来使用脚本,变量和类型(就像jQuery和$一样):

const bar = Foo.bar;
const meaningOfLife: Foo.MeaningOfLife = 42;

使用导入类型语法导入bar类型的脚本:

const baz: typeof import ('foo').bar = 'hello';

一个模块bar变量是使用命名的import导入的。

import { bar } from 'foo';

bar.toUpperCase();

一个模块,整个包作为名称空间导入:

import * as foo from 'foo';

foo.bar.toUpperCase();

全局范围与窗口

如果正确键入了此类库,则作为使用者的您无需执行任何操作即可使其正常工作。它会自动在您的全局范围内可用,不需要为Window进行扩充。

但是,如果您想将库的内容显式地附加到window上,也可以这样做:

declare global {
  interface Window {
    Foo: typeof import('foo');
  }
}

window.Foo.bar;