Sind stark typisierte Funktionen als Parameter in TypeScript möglich?

559

In TypeScript kann ich einen Parameter einer Funktion als Typfunktion deklarieren. Gibt es eine "typsichere" Möglichkeit, die mir fehlt? Betrachten Sie zum Beispiel Folgendes:

class Foo {
    save(callback: Function) : void {
        //Do the save
        var result : number = 42; //We get a number from the save operation
        //Can I at compile-time ensure the callback accepts a single parameter of type number somehow?
        callback(result);
    }
}

var foo = new Foo();
var callback = (result: string) : void => {
    alert(result);
}
foo.save(callback);

Der Rückruf zum Speichern ist nicht typsicher. Ich gebe ihm eine Rückruffunktion, bei der der Parameter der Funktion eine Zeichenfolge ist, aber ich übergebe ihm eine Nummer und kompiliere ohne Fehler. Kann ich den Ergebnisparameter beim Speichern einer typsicheren Funktion festlegen?

TL; DR-Version: Gibt es ein Äquivalent zu einem .NET-Delegaten in TypeScript?

vcsjones
quelle

Antworten:

805

Sicher. Der Typ einer Funktion besteht aus den Typen ihres Arguments und ihrem Rückgabetyp. Hier geben wir an, dass der callbackTyp des Parameters "Funktion, die eine Zahl akzeptiert und Typ zurückgibt any" sein muss:

class Foo {
    save(callback: (n: number) => any) : void {
        callback(42);
    }
}
var foo = new Foo();

var strCallback = (result: string) : void => {
    alert(result);
}
var numCallback = (result: number) : void => {
    alert(result.toString());
}

foo.save(strCallback); // not OK
foo.save(numCallback); // OK

Wenn Sie möchten, können Sie einen Typalias definieren , um dies zu kapseln:

type NumberCallback = (n: number) => any;

class Foo {
    // Equivalent
    save(callback: NumberCallback) : void {
        callback(42);
    }
}
Ryan Cavanaugh
quelle
6
(n: number) => anybedeutet eine Funktionssignatur?
Nikk Wong
16
@nikkwong bedeutet, dass die Funktion einen Parameter (a number) akzeptiert, aber der Rückgabetyp überhaupt nicht eingeschränkt ist (kann ein beliebiger Wert sein oder sogar void)
Daniel Earwicker
16
Was ist der Sinn ndieser Syntax? Wären die Eingabe- und Ausgabetypen allein nicht ausreichend?
Yuhuan Jiang
4
Ein Nebeneffekt zwischen der Verwendung von Inline-Funktionen und benannten Funktionen (Antwort unten gegenüber dieser Antwort) ist, dass die Variable "this" mit der benannten Funktion nicht definiert ist, während sie innerhalb der Inline-Funktion definiert ist. Keine Überraschung für JavaScript-Codierer, aber für andere Codierungshintergründe definitiv nicht offensichtlich.
Stevko
3
@YuhuanJiang Dieser Beitrag könnte für Sie von Interesse sein
Ophidian
93

Hier sind TypeScript-Entsprechungen einiger gängiger .NET-Delegaten:

interface Action<T>
{
    (item: T): void;
}

interface Func<T,TResult>
{
    (item: T): TResult;
}
Drew Noakes
quelle
2
Wahrscheinlich nützlich anzusehen, aber es wäre ein Anti-Muster, solche Typen tatsächlich zu verwenden. Auf jeden Fall ähneln diese eher Java-SAM-Typen als C # -Delegierten. Natürlich sind sie es nicht und sie entsprechen der Typ-Alias-Form, die für Funktionen nur eleganter ist
Aluan Haddad,
5
@AluanHaddad Könnten Sie näher erläutern, warum Sie dies für ein Anti-Muster halten?
Max R McCarty
8
Der Grund dafür ist, dass TypeScript über eine präzise Literal-Syntax vom Funktionstyp verfügt, die solche Schnittstellen überflüssig macht. In C # sind Delegaten nominal, aber die Actionund Func-Delegierten machen den größten Teil der Notwendigkeit für bestimmte Delegatentypen überflüssig und geben C # interessanterweise den Anschein einer strukturellen Typisierung. Der Nachteil dieser Delegierten ist, dass ihre Namen keine Bedeutung haben, aber die anderen Vorteile überwiegen im Allgemeinen. In TypeScript brauchen wir diese Typen einfach nicht. Das Anti-Muster wäre also function map<T, U>(xs: T[], f: Func<T, U>). Bevorzugenfunction map<T, U>(xs: T[], f: (x: T) => U)
Aluan Haddad
6
Es ist Geschmackssache, da dies äquivalente Formen in einer Sprache sind, die keine Laufzeitarten hat. Heutzutage können Sie anstelle von Schnittstellen auch Typ-Aliase verwenden.
Drew Noakes
18

Mir ist klar, dass dieser Beitrag alt ist, aber es gibt einen kompakteren Ansatz, der sich geringfügig von dem unterscheidet, was gefragt wurde, aber möglicherweise eine sehr hilfreiche Alternative darstellt. Sie können die Funktion im Wesentlichen inline deklarieren, wenn Sie die Methode aufrufen ( in diesem Fall Foos save()). Es würde ungefähr so ​​aussehen:

class Foo {
    save(callback: (n: number) => any) : void {
        callback(42)
    }

    multipleCallbacks(firstCallback: (s: string) => void, secondCallback: (b: boolean) => boolean): void {
        firstCallback("hello world")

        let result: boolean = secondCallback(true)
        console.log("Resulting boolean: " + result)
    }
}

var foo = new Foo()

// Single callback example.
// Just like with @RyanCavanaugh's approach, ensure the parameter(s) and return
// types match the declared types above in the `save()` method definition.
foo.save((newNumber: number) => {
    console.log("Some number: " + newNumber)

    // This is optional, since "any" is the declared return type.
    return newNumber
})

// Multiple callbacks example.
// Each call is on a separate line for clarity.
// Note that `firstCallback()` has a void return type, while the second is boolean.
foo.multipleCallbacks(
    (s: string) => {
         console.log("Some string: " + s)
    },
    (b: boolean) => {
        console.log("Some boolean: " + b)
        let result = b && false

        return result
    }
)

Der multipleCallback()Ansatz ist sehr nützlich für Dinge wie Netzwerkanrufe, die erfolgreich sein oder fehlschlagen können. Unter der Annahme eines Beispiels multipleCallbacks()für einen Netzwerkaufruf kann beim Aufruf das Verhalten für Erfolg und Misserfolg an einer Stelle definiert werden, was für zukünftige Codeleser zu größerer Klarheit führt.

Nach meiner Erfahrung bietet sich dieser Ansatz im Allgemeinen an, um prägnanter, weniger übersichtlich und insgesamt klarer zu sein.

Viel Glück euch allen!

kbpontius
quelle
16
type FunctionName = (n: inputType) => any;

class ClassName {
    save(callback: FunctionName) : void {
        callback(data);
    }
}

Dies stimmt sicherlich mit dem funktionalen Programmierparadigma überein.

Krishna Ganeriwal
quelle
6
Sie sollten es inputTypeeher nennen als returnType, oder? Wo inputTypeist der Typ, von datadem Sie einen Parameter an die callbackFunktion übergeben?
ChrisW
Ja @ChrisW Sie haben Recht, inputType ist sinnvoller. Vielen Dank!
Krishna Ganeriwal
2

In TS können wir Funktionen auf folgende Weise eingeben:

Funktionstypen / Signaturen

Dies wird für reale Implementierungen von Funktionen / Methoden verwendet und hat die folgende Syntax:

(arg1: Arg1type, arg2: Arg2type) : ReturnType

Beispiel:

function add(x: number, y: number): number {
    return x + y;
}

class Date {
  setTime(time: number): number {
   // ...
  }

}

Funktionstyp-Literale

Funktionstyp-Literale sind eine weitere Möglichkeit, den Typ einer Funktion zu deklarieren. Sie werden normalerweise in der Funktionssignatur einer Funktion höherer Ordnung angewendet. Eine Funktion höherer Ordnung ist eine Funktion, die Funktionen als Parameter akzeptiert oder eine Funktion zurückgibt. Es hat die folgende Syntax:

(arg1: Arg1type, arg2: Arg2type) => ReturnType

Beispiel:

type FunctionType1 = (x: string, y: number) => number;

class Foo {
    save(callback: (str: string) => void) {
       // ...
    }

    doStuff(callback: FunctionType1) {
       // ...
    }

}
Willem van der Veen
quelle
1

Wenn Sie zuerst den Funktionstyp definieren, sieht er so aus

type Callback = (n: number) => void;

class Foo {
    save(callback: Callback) : void {        
        callback(42);
    }
}

var foo = new Foo();
var stringCallback = (result: string) : void => {
    console.log(result);
}

var numberCallback = (result: number) : void => {
    console.log(result);
}

foo.save(stringCallback); //--will be showing error
foo.save(numberCallback);

Ohne Funktionstyp unter Verwendung der einfachen Eigenschaftssyntax wäre dies:

class Foo {
    save(callback: (n: number) => void) : void {        
        callback(42);
    }
}

var foo = new Foo();
var stringCallback = (result: string) : void => {
    console.log(result);
}

var numberCallback = (result: number) : void => {
    console.log(result);
}

foo.save(stringCallback); //--will be showing error
foo.save(numberCallback);

Wenn Sie eine Schnittstellenfunktion wie generische c # -Delegierte verwenden möchten, wäre dies:

interface CallBackFunc<T, U>
{
    (input:T): U;
};

class Foo {
    save(callback: CallBackFunc<number,void>) : void {        
        callback(42);
    }
}

var foo = new Foo();
var stringCallback = (result: string) : void => {
    console.log(result);
}

var numberCallback = (result: number) : void => {
    console.log(result);
}

let strCBObj:CallBackFunc<string,void> = stringCallback;
let numberCBObj:CallBackFunc<number,void> = numberCallback;

foo.save(strCBObj); //--will be showing error
foo.save(numberCBObj);
Humayoun_Kabir
quelle
0

Neben dem, was andere sagten, besteht ein häufiges Problem darin, die Typen derselben Funktion zu deklarieren, die überladen sind. Ein typischer Fall ist die EventEmitter on () -Methode, die mehrere Arten von Listenern akzeptiert. Ähnliches kann passieren, wenn Sie mit Redux-Aktionen arbeiten - und dort verwenden Sie den Aktionstyp als Literal, um die Überladung zu markieren. Bei EventEmitters verwenden Sie den Literaltyp Ereignisname:

interface MyEmitter extends EventEmitter {
  on(name:'click', l: ClickListener):void
  on(name:'move', l: MoveListener):void
  on(name:'die', l: DieListener):void
  //and a generic one
  on(name:string, l:(...a:any[])=>any):void
}

type ClickListener = (e:ClickEvent)=>void
type MoveListener = (e:MoveEvent)=>void
... etc

// will type check the correct listener when writing something like:
myEmitter.on('click', e=>...<--- autocompletion
Cancerbero
quelle