typescript - hur man använder prototypen av en PromiseConstructorLike-instans i gränssnittsdeklaration?


Jag försöker skriva deklarationer för Google Maps hjälparmodul för node, men jag har problem med PromiseConstructorLike som biblioteket förväntar sig, och att returnera dess "PromiseLike" instansmetoder korrekt (enligt https://googlemaps.github.io/google-maps-services-js/docs/module-@google_maps.html):

Promise     function    <optional>  Promise constructor (optional).

så jag gjorde det (avskalat till de intressanta delarna):

declare namespace GoogleMaps {
  export interface CreateClientOptions<T> {
    /** Promise constructor (optional). */
    Promise?: T; 
  }

  export interface GoogleMapsClient<T> {
    directions<U>(query, callback?: ResponseCallback<U>): RequestHandle<U, T>;
  }

  export interface Response<U extends any> {
      headers: any;
      json: U;
      status: number;
  }

  export interface RequestHandle<U, T extends PromiseLike<Response<U>>> {
      asPromise(): T;
      cancel(): void;
      finally(callback: ResponseCallback<U>): void;
  }

  export type ResponseCallback<U> = (err: Error, result: Response<U>) => void; 
  export function createClient<T extends PromiseConstructorLike>(options: CreateClientOptions<T>): GoogleMapsClient<T>;
}

declare module '@google/maps' {
  export = GoogleMaps
}

självklart fungerar det inte, om jag till exempel använder Bluebird i createClient som

import * as bluebird from 'bluebird'
import { createClient } from '@google/maps'

createClient({ Promise: bluebird }).directions({}).asPromise()/** no "then" here, just the static methods from Bluebird, like Bluebird.all */

Frågan är då:

Finns det något sätt jag kan antyda för asPromise-metoden att returnera instansmetoderna (then, catch, finally, reduce, timeout, etc) från Bluebird utan att manuellt behöva utöka RequestHandle-gränssnittet?

Mer information (lib.d.ts-deklarationer):

PromiseConstructorLike är:

 declare type PromiseConstructorLike = new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) => PromiseLike<T>;

PromiseLike är:

interface PromiseLike<T> {
    /**
     * Attaches callbacks for the resolution and/or rejection of the Promise.
     * @param onfulfilled The callback to execute when the Promise is resolved.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of which ever callback is executed.
     */
    then(
        onfulfilled?: ((value: T) => T | PromiseLike<T>) | undefined | null,
        onrejected?: ((reason: any) => T | PromiseLike<T>) | undefined | null): PromiseLike<T>;
}

Svar #1

Dina deklarationer innehåller ett kompilationsfel, vilket härstammar från att blanda ihop Promise instans typen och Promise konstruktör typen. Typparametern T i GoogleMapsClient används för att fylla T i RequestHandle, men i GoogleMapsClient representerar detta Promise-konstruktörstypen, medan i RequestHandle representerar det Promise-instanstypen.

Det verkar vara din avsikt att göra allt korrekt typat när det gäller Promise instans-typen, PromiseLike<Response<U>>, där U är responstypen. Men eftersom U inte är känt i förväg (d.v.s. innan GoogleMapsClient.directions anropas), är detta tyvärr inte möjligt.

Om du vill kalla then() efter asPromise() kan du helt enkelt ändra returtypen för RequestHandle.asPromise till PromiseLike<Response<U>> och ta bort typparametern T:

export interface RequestHandle<U> {
    asPromise(): PromiseLike<U>;
    cancel(): void;
    finally(callback: ResponseCallback<U>): void;
}

Jag skulle personligen också lägga till begränsningen extends PromiseConstructorLike på typparametern T i både CreateClientOptions och GoogleMapsClient, så att typ-säkerheten för den passerade Promise-konstruktören inte enbart beror på begränsningen som anges i createClient.

Sammanfattningsvis ser deklarationerna nu ut så här:

declare namespace GoogleMaps {
  export interface CreateClientOptions<T extends PromiseConstructorLike> {
    /** Promise constructor (optional). */
    Promise?: T; 
  }

  export interface GoogleMapsClient<T extends PromiseConstructorLike> {
    directions<U>(query, callback?: ResponseCallback<U>): RequestHandle<U>;
  }

  export interface Response<U extends any> {
      headers: any;
      json: U;
      status: number;
  }

  export interface RequestHandle<U> {
      asPromise(): PromiseLike<Response<U>>;
      cancel(): void;
      finally(callback: ResponseCallback<U>): void;
  }

  export type ResponseCallback<U> = (err: Error, result: Response<U>) => void; 
  export function createClient<T extends PromiseConstructorLike>(options: CreateClientOptions<T>): GoogleMapsClient<T>;
}

declare module '@google/maps' {
  export = GoogleMaps
}

Med dessa deklarationer fungerar ditt bluebird-exempel och du kan kalla then() efter asPromise().

Svar #2

med lanseringen av Typescript 2.8 gör det nya "infer"-nyckelordet detta möjligt! det kan härleda (och vidarebefordra) invecklade och nästlade deklarationer som tolken försöker hämta informationen från åt dig, vilket ger en riktigt bra starkt typad upplevelse.

så, om du vill få typen på konstruktören

class MyPromise extends Promise<any> implements PromiseLike<any> {
    add(s: number) {
        s++
        return this
    }
    dummy() {
        return this
    }
}

function typedFactory<
    U extends PromiseConstructorLike,
>(u: U): InstanceType<U> {
    return new u<void>(() => { }) as any 
    // this isn't needed since we are just trying to show the functionality, 
    // would be interfacing another library through types only, so that 
    // the compiler doesn't b*tch about it
}

typedFactory(Promise).then(() => { })
typedFactory(MyPromise).add(1).dummy().then(() => {})

den nya InstanceType finns faktiskt i lib.es5.d.ts och definieras som:

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

det visar den verkliga kraften hos infer-nyckelordet, och du kan prova det på https://www.typescriptlang.org/play/

Relaterade artiklar