Hur överbelastar man en funktion i TypeScript med valfria parametrar?


Jag har följande kod som delvis har konverterats till TypeScript (från JavaScript).

I princip, om en callback-parameter finns, vill jag alltid att funktionen själv ska returnera void. Annars, returtyp Promise<object>. Med en valfri parameter (settings) innan det (så tekniskt sett kan callback-parametern skickas in som settings-parametern, vilket de första raderna i funktionen hanterar det användningsfallet).

Av bakåtkompatibilitetsskäl (och för att hålla koden DRY) vill jag inte skapa en annan funktion som heter savePromise eller saveCallback och separera den. Jag försöker lista ut hur man får TypeScript att vara tillräckligt smart för att förstå den här logiken på något sätt.

type CallbackType<T, E> = (response: T | null, error?: E) => void;

class User {
    save(data: string, settings?: object, callback?: CallbackType<object, string>): Promise<object> | void {
        if (typeof settings === "function") {
            callback = settings;
            settings = undefined;
        }

        if (callback) {
            setTimeout(() => {
                callback({"id": 1, "settings": settings});
            }, 1000);
        } else {
            return new Promise((resolve) => {
                setTimeout(() => {
                    resolve({"id": 1, "settings": settings});
                }, 1000);
            });
        }
    }
}

const a = new User().save("Hello World"); // Should be type Promise<object>, should eventually resolve to {"id": 1, "settings": undefined}
const b = new User().save("Hello World", (obj) => {
    console.log(obj); // {"id": 1, "settings": undefined}
}); // Should be type void
const c = new User().save("Hello World", {"log": true}); // Should be type Promise<object>, should eventually resolve to {"id": 1, "settings": {"log": true}}
const d = new User().save("Hello World", {"log": true}, (obj) => {
    console.log(obj); // {"id": 1, "settings": {"log": true}}
}); // Should be type void

Jag är ganska säker på att den typ av fil jag siktar på skulle vara något i stil med följande. Inte säker på att jag är exakt här dock.

save(data: string, settings?: object): Promise<object>;
save(data: string, callback: CallbackType<object, string>): void;
save(data: string, settings: object, callback: CallbackType<object, string>): void;

Det verkar som att callback-parametern som skickas in som settings-parameterns användningsfall kan hanteras genom att göra något liknande:

save(data: string, settings?: object | CallbackType<object, string>, callback?: CallbackType<object, string>): Promise<object> | void

Men det är superstökigt, och från min erfarenhet verkar det inte som att TypeScript är tillräckligt smart för att inse att settings alltid kommer att vara ett valfritt objekt efter de första 4 raderna kod i funktionen. Vilket betyder att när man anropar callback måste man göra en typomvandling, vilket återigen känns väldigt stökigt.

Hur kan jag uppnå detta med TypeScript?

Svar #1

Sammanfattning

Här är lösningen (med viss omskrivning):

type CallbackType<T, E> = (response: T | null, error?: E) => void;
interface ISettings {
  log?: boolean;
}
interface ISaveResult {
  id: number;
  settings: ISettings | undefined;
}

class User {
  save(data: string): Promise<ISaveResult>;
  save(data: string, settings: ISettings): Promise<ISaveResult>;
  save(data: string, callback: CallbackType<ISaveResult, string>): void;
  save(data: string, settings: ISettings, callback: CallbackType<ISaveResult, string>): void;
  save(data: string, settings?: ISettings | CallbackType<ISaveResult, string>, callback?: CallbackType<ISaveResult, string>): Promise<ISaveResult> | void {
    if (typeof settings !== "object" && typeof settings !== "undefined") {
      callback = settings;
      settings = undefined;
    }

    const localSettings = settings; // required for closure compatibility
    if (callback) {
      const localCallback = callback; // required for closure compatibility
      setTimeout(() => {
        localCallback({ id: 1, "settings": localSettings });
      }, 1000);
    } else {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({ id: 1, "settings": localSettings });
        }, 1000);
      });
    }
  }
}

const a = new User().save("Hello World"); // User.save(data: string): Promise<ISaveResult>

const b = new User().save("Hello World", obj => { 
  console.log(obj); // obj: ISaveResult | null
}); // User.save(data: string, callback: CallbackType<ISaveResult, string>): void

const c = new User().save("Hello World", { "log": true }); // User.save(data: string, settings: ISettings): Promise<ISaveResult>

const d = new User().save("Hello World", { "log": true }, (obj) => {
  console.log(obj); // obj: ISaveResult | null
}); // User.save(data: string, settings: ISettings, callback: CallbackType<ISaveResult, string>): void

se kod panna

Förklaring

en Function är också ett Object, så TypeScript kunde inte implicit skilja mellan de två. Genom att skapa ett specifikt ISettings-gränssnitt tillåter du TypeScript att skilja mellan ett inställningsobjekt och en callback-funktion.

Det enklaste sättet att se det är att titta på de fel som TypeScript visar och typerna på variablerna när kodflödet fortskrider, t.ex. (din kod):

  • När du hovrar över settings i if-villkoret:

  • När du hovrar över settings inuti if-blocket:

  • callback tilldelningsfel:

    Typen 'Function' kan inte tilldelas typen 'CallbackType'. Typen 'Function' matchar inte signaturen '(response: object | null, error?: string | undefined): void'.(2322)

Relaterade artiklar