javascript - Hur man använder ett d.ts-fils exporterade const-typer om bibliotekets implementation inte är integrerad med TS-projektet?


DefinitelyTyped har typdefinitioner för många bibliotek, men ofta nog kan jag inte hitta ett bra sätt att använda dem när Javascript-implementeringen är separerad från Typescript, som när ett bibliotek tilldelar sig själv till en egenskap av fönstret genom en

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

tagg, och när JS-bunten jag hanterar finns i ett annat separat skript. (Även om det att bunta ihop allt inklusive biblioteket är den standardmässiga och pålitliga metoden, anta för frågans skull att jag inte har möjlighet att importera biblioteket till mitt TS-projekt på riktigt.) Till exempel, säg att jag hittar en snygg definitionsfil för ett bibliotek som heter 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;

I JS kan jag använda myLib genom att anropa window.myLib.doThing1() och window.myLib.doThing2(). Hur kan jag importera formen av hela window.myLib-objektet så att jag kan deklarera det som en egenskap av window? Jag kan se att jag kan importera de exporterade gränssnitten, t.ex:

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

Detta fungerar, men jag vill ha tillgång till formen av det faktiska biblioteksobjektet och dess egenskapsvärden (funktionerna och strängarna och liknande), inte bara gränssnitten. Om jag gör

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

då blir myLib-identifieraren ett namnrymd, från vilken jag kan referera till de exporterade gränssnitten, men precis som ovan har jag fortfarande ingen åtkomst till export const och export function-formerna från my-lib.d.ts. (Och, förstås, att försöka använda den importerade namnrymden för att deklarera bibliotekets objekt fungerar inte: Kan inte använda namnrymden 'myLib' som en typ. Även om jag skulle kunna göra det, skulle det inte nödvändigtvis vara säkert, eftersom biblioteket paketerat för webbläsaren mycket väl kan vara strukturerat något annorlunda än bibliotekets Node-exportobjekt)

Om jag manuellt kopierar och klistrar in delar av d.ts i mitt eget skript kan jag sätta ihop något som fungerar:

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

Men det här är rörigt, tidskrävande och säkert inte det riktiga sättet att göra något sådant på. När jag stöter på den här typen av situation vill jag göra något kort och elegant som:

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

Vissa definitionsfiler inkluderar ett gränssnitt för bibliotekets objekt, som jQuery, vilket gör:

// index.d.ts
/// <reference path="JQuery.d.ts" />

// jQuery.d.ts
interface JQuery<TElement = HTMLElement> extends Iterable<TElement> {
  // lots and lots of definitions

Då är allt bara bra - jag kan bara använda interface Window { $: jQuery }, men många bibliotek som inte ursprungligen skapats för användning i webbläsare erbjuder inte ett sådant gränssnitt.

Som nämnts tidigare skulle den bästa lösningen vara att bibliotekets implementation integreras med TS-projektet, vilket gör det möjligt att både importera och använda biblioteket och dess typer utan problem, men om det inte är möjligt, har jag några bra alternativ kvar? Jag skulle kunna undersöka egenskaperna på det riktiga bibliotekets objekt och lägga till ett interface i definitionsfilen som inkluderar alla sådana egenskaper och deras typer, men att behöva ändra den semi-kanoniska källdefinitionsfilen som accepteras av DT och används av alla andra känns fel. Jag skulle föredra att kunna importera formerna av exporterna i definitionsfilen och skapa ett interface från dem utan att ändra den ursprungliga filen, men det kanske inte är möjligt.

Finns det en mer elegant lösning, eller är de definitionsfiler jag råkat stöta på helt enkelt otillräckligt lämpade för mitt mål och måste därför modifieras?

Svar #1

Om modulen har export as namespace myLib så exporterar modulen redan biblioteket som ett globalt objekt. Så du kan bara använda biblioteket som:

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

Detta är sant så länge filen du använder biblioteket i inte är en modul själv (dvs den innehåller inga import och inga export-satser).

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)

Du kan lägga till en egenskap på Window som är av samma typ som typen av biblioteket genom att använda en importtyp (lade till i 2.9 tror jag)

// 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

Redigera

Tydligen har Typescript-teamet faktiskt arbetat på något för just detta problem. Som du kan läsa i denna PR kommer nästa version av Typescript att inkludera en allowUmdGlobalAccess-flagga. Denna flagga kommer att tillåta åtkomst till UMD-modulglobala variabler från moduler. Med denna flagga inställd på true kommer denna kod att vara giltig:

export {} // module now
let a:myLib.AnInterface; // Types are still ok without the import
let b =  myLib.doThing1(); // ok, on [email 

Detta betyder att du bara kan komma åt modulens export utan att behöva använda window. Detta kommer att fungera om den globala exporten är kompatibel med webbläsaren, vilket jag skulle förvänta mig att den är.

Svar #2

Vad handskas du med

när ett bibliotek tilldelar sig själv till en egenskap hos fönstret

Det kallas ett UMD-paket. Dessa är de som används genom att lägga till en länk inuti en <script />-tagg i ditt dokument, och de fäster sig själva vid den globala omfattningen.

UMD-paket behöver inte konsumeras på detta sätt — de kan också konsumeras som moduler, med ett import (eller require) uttalande.

TypeScript stöder båda användningarna.

Hur bör UMD-paket typas

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

export as namespace Foo;
export = Foo;

Denna definition berättar för TypeScript att:

  • om den fil som används är ett script, då finns det en variabel som heter bar i det globala omfånget
  • om den konsumera filen är en modul, då kan du importera bar med namngivna importer eller importera hela namnutrymmet med wildcard (*) import.

Vad är skillnaden mellan ett script och ett module?

Ett script skulle vara en bit JavaScript som körs inne i en <script />-tagg i ditt HTML-dokument. Det kan vara inbäddat eller laddas från en fil. Det är så JavaScript alltid har använts i webbläsaren.

En modul är en JavaScript- (TypeScript-) fil som har minst ett import- eller export-uttalande. De är en del av ECMAScript-standarden och stöds inte överallt. Vanligtvis skapar du moduler i ditt projekt och låter en bundler som Webpack skapa ett paket för din applikation att använda.

Att konsumera UMD-paket

Ett skript, variabler och typer används genom att komma åt det globala Foo-namnrymden (precis som jQuery och $):

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

Ett skript, typen av bar importeras med hjälp av import type-syntaksen:

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

Ett modul, bar-variabeln importeras med en namngiven import.

import { bar } from 'foo';

bar.toUpperCase();

Ett modul, hela paketet importeras som ett namnrymd:

import * as foo from 'foo';

foo.bar.toUpperCase();

Globalt omfång vs. window

Om ett sådant bibliotek är korrekt typat behöver du som användare inte göra någonting för att få det att fungera. Det kommer automatiskt att vara tillgängligt i din globala scope och ingen utökning för Window kommer att vara nödvändig.

Men om du vill bifoga innehållet i ditt bibliotek till window explicit, kan du också göra det:

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

window.Foo.bar;

Relaterade artiklar