Les fonctions fortement typées en tant que paramètres sont-elles possibles dans TypeScript?

560

Dans TypeScript, je peux déclarer un paramètre d'une fonction en tant que type Function. Existe-t-il un moyen «sûr de type» de faire cela qui me manque? Par exemple, considérez ceci:

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);

Le rappel de sauvegarde n'est pas de type sécurisé, je lui donne une fonction de rappel où le paramètre de la fonction est une chaîne mais je lui passe un nombre et compile sans erreur. Puis-je faire le paramètre de résultat dans enregistrer une fonction de type sécurisé?

Version TL; DR: existe-t-il un équivalent d'un délégué .NET en TypeScript?

vcsjones
la source

Réponses:

805

Sûr. Le type d' une fonction se compose des types de son argument et de son type de retour. Ici, nous spécifions que le callbacktype du paramètre doit être "fonction qui accepte un nombre et renvoie un type any":

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

Si vous le souhaitez, vous pouvez définir un alias de type pour encapsuler ceci:

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

class Foo {
    // Equivalent
    save(callback: NumberCallback) : void {
        callback(42);
    }
}
Ryan Cavanaugh
la source
6
(n: number) => anysignifie toute signature de fonction?
nikk wong
16
@nikkwong cela signifie que la fonction prend un paramètre (a number) mais le type de retour n'est pas du tout limité (peut être n'importe quelle valeur, ou même void)
Daniel Earwicker
16
À quoi sert ncette syntaxe? Les types d'entrée et de sortie ne suffiraient-ils pas à eux seuls?
Yuhuan Jiang
4
Un effet secondaire entre l'utilisation des fonctions en ligne et des fonctions nommées (réponse ci-dessous par rapport à cette réponse) est que la variable "this" n'est pas définie avec la fonction nommée alors qu'elle est définie dans la fonction en ligne. Pas de surprise pour les codeurs JavaScript, mais certainement pas évident pour les autres arrière-plans de codage.
Stevko
3
@YuhuanJiang Ce message pourrait vous intéresser
Ophidian
93

Voici les équivalents TypeScript de certains délégués .NET courants:

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

interface Func<T,TResult>
{
    (item: T): TResult;
}
Drew Noakes
la source
2
Probablement utile à regarder, mais ce serait un anti-modèle d'utiliser réellement de tels types. Quoi qu'il en soit, ceux-ci ressemblent plus à des types Java SAM qu'à des délégués C #. Bien sûr, ils ne le sont pas et ils sont équivalents à la forme d'alias de type qui est juste plus élégante pour les fonctions
Aluan Haddad
5
@AluanHaddad pourriez-vous expliquer pourquoi vous pensez que c'est un anti-modèle?
Max R McCarty
8
La raison en est que TypeScript a une syntaxe littérale de type de fonction concise qui évite le besoin de telles interfaces. En C # délégués sont nominales, mais les Actionet les Funcdélégués à la fois parer à la plupart des types de besoin de délégué spécifique et, fait intéressant, donnent C # un semblant de structure de frappe. L'inconvénient de ces délégués est que leur nom n'a aucune signification, mais les autres avantages l'emportent généralement sur cela. Dans TypeScript, nous n'avons tout simplement pas besoin de ces types. L'anti-modèle serait donc function map<T, U>(xs: T[], f: Func<T, U>). Préférerfunction map<T, U>(xs: T[], f: (x: T) => U)
Aluan Haddad
6
C'est une question de goût, car ce sont des formes équivalentes dans un langage qui n'a pas de types d'exécution. De nos jours, vous pouvez également utiliser des alias de type au lieu d'interfaces.
Drew Noakes
18

Je me rends compte que ce message est ancien, mais il existe une approche plus compacte légèrement différente de ce qui a été demandé, mais peut être une alternative très utile. Vous pouvez essentiellement déclarer la fonction en ligne lors de l' appel de la méthode ( Foo« s save()dans ce cas). Cela ressemblerait à quelque chose comme ceci:

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
    }
)

L' multipleCallback()approche est très utile pour des choses comme les appels réseau qui peuvent réussir ou échouer. En supposant à nouveau un exemple d'appel réseau, lorsqu'il multipleCallbacks()est appelé, le comportement à la fois pour un succès et un échec peut être défini en un seul endroit, ce qui se prête à une plus grande clarté pour les futurs lecteurs de code.

En général, d'après mon expérience, cette approche se prête à être plus concise, moins encombrante et plus claire dans l'ensemble.

Bonne chance à tous!

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

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

Cela correspond certainement au paradigme de programmation fonctionnelle.

Krishna Ganeriwal
la source
6
Vous devriez l'appeler inputTypeplutôt que returnType, n'est-ce pas? Où inputTypeest le type datadont vous passez un paramètre à la callbackfonction.
ChrisW
Oui @ChrisW vous avez raison, inputType a plus de sens. Merci!
Krishna Ganeriwal
2

Dans TS, nous pouvons taper les fonctions de la manière suivante:

Types de fonctions / signatures

Ceci est utilisé pour des implémentations réelles de fonctions / méthodes, il a la syntaxe suivante:

(arg1: Arg1type, arg2: Arg2type) : ReturnType

Exemple:

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

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

}

Type de fonction Littéraux

Les littéraux de type de fonction sont une autre façon de déclarer le type d'une fonction. Ils sont généralement appliqués dans la signature de fonction d'une fonction d'ordre supérieur. Une fonction d'ordre supérieur est une fonction qui accepte des fonctions comme paramètres ou qui renvoie une fonction. Il a la syntaxe suivante:

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

Exemple:

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

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

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

}
Willem van der Veen
la source
1

Si vous définissez d'abord le type de fonction, il ressemblerait à

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);

Sans type de fonction en utilisant la syntaxe de propriété simple, ce serait:

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);

Si vous voulez en utilisant une fonction d'interface comme les délégués génériques c #, ce serait:

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
la source
0

Outre ce que d'autres ont dit, un problème commun est de déclarer les types de la même fonction qui est surchargée. Le cas typique est la méthode EventEmitter on () qui accepte plusieurs types d'écouteurs. Une situation similaire peut se produire lorsque vous travaillez avec des actions redux - et là, vous utilisez le type d'action comme littéral pour marquer la surcharge. Dans le cas d'EventEmitters, vous utilisez le type littéral du nom de l'événement:

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
la source