¿Son posibles las funciones fuertemente tipadas como parámetros en TypeScript?

560

En TypeScript, puedo declarar un parámetro de una función como un tipo Función. ¿Hay una forma "segura de escribir" de hacer esto que me falta? Por ejemplo, considere esto:

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

La devolución de llamada de guardar no es de tipo seguro, le estoy dando una función de devolución de llamada donde el parámetro de la función es una cadena pero le estoy pasando un número y se compila sin errores. ¿Puedo hacer que el parámetro de resultado guarde una función de tipo seguro?

TL; Versión DR: ¿hay un equivalente de un delegado .NET en TypeScript?

vcsjones
fuente

Respuestas:

805

Por supuesto. El tipo de una función consiste en los tipos de su argumento y su tipo de retorno. Aquí especificamos que el callbacktipo de parámetro debe ser "función que acepta un número y devuelve tipo 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 lo desea, puede definir un alias de tipo para encapsular esto:

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

class Foo {
    // Equivalent
    save(callback: NumberCallback) : void {
        callback(42);
    }
}
Ryan Cavanaugh
fuente
66
(n: number) => anysignifica cualquier firma de función?
Nikk Wong
16
@nikkwong significa que la función toma un parámetro (a number) pero el tipo de retorno no está restringido en absoluto (podría ser cualquier valor, o incluso void)
Daniel Earwicker
16
¿Para qué sirve nesta sintaxis? ¿No serían suficientes solo los tipos de entrada y salida?
Yuhuan Jiang
44
Un efecto secundario entre el uso de funciones en línea frente a funciones con nombre (respuesta a continuación frente a esta respuesta) es que la variable "este" no está definida con la función con nombre, mientras que está definida dentro de la función en línea. No es una sorpresa para los codificadores de JavaScript, pero definitivamente no es obvio para otros fondos de codificación.
Stevko
3
@YuhuanJiang Este mensaje podría ser de interés para usted
Ophidian
93

Aquí hay equivalentes de TypeScript de algunos delegados comunes de .NET:

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

interface Func<T,TResult>
{
    (item: T): TResult;
}
Drew Noakes
fuente
2
Probablemente sea útil mirarlo, pero sería un antipatrón usar tales tipos. De todos modos, se parecen más a los tipos SAM de Java que a los delegados de C #. Por supuesto que no lo son y son equivalentes a la forma de alias tipo que es simplemente más elegante para las funciones
Aluan Haddad
55
@AluanHaddad, ¿podría explicar por qué pensaría que esto es un antipatrón?
Max R McCarty
8
La razón es que TypeScript tiene una sintaxis literal de tipo de función concisa que evita la necesidad de tales interfaces. En C # delegados son nominales, pero las Actiony Funclos delegados tanto obviate la mayor parte de la necesidad de tipos específicos de delegados y, curiosamente, dan C # una de apariencia de tipificación estructural. La desventaja de estos delegados es que sus nombres no tienen sentido, pero las otras ventajas generalmente superan esto. En TypeScript simplemente no necesitamos estos tipos. Así sería el antipatrón function map<T, U>(xs: T[], f: Func<T, U>). Prefierofunction map<T, U>(xs: T[], f: (x: T) => U)
Aluan Haddad
66
Es una cuestión de gustos, ya que estas son formas equivalentes en un lenguaje que no tiene tipos de tiempo de ejecución. Hoy en día también puede usar alias de tipo en lugar de interfaces.
Drew Noakes
18

Me doy cuenta de que esta publicación es antigua, pero hay un enfoque más compacto que es ligeramente diferente de lo que se pidió, pero puede ser una alternativa muy útil. Se puede declarar esencialmente la función en línea al llamar al método ( Foo's save()en este caso). Se vería algo así:

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

El multipleCallback()enfoque es muy útil para cosas como llamadas de red que pueden tener éxito o fallar. Una vez más, suponiendo un ejemplo de llamada de red, cuando multipleCallbacks()se llama, el comportamiento tanto para el éxito como para el fracaso se puede definir en un solo lugar, lo que se presta a una mayor claridad para futuros lectores de códigos.

En general, en mi experiencia, este enfoque se presta a ser más conciso, menos desorden y mayor claridad en general.

¡Buena suerte a todos!

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

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

Esto seguramente se alinea con el paradigma de programación funcional.

Krishna Ganeriwal
fuente
66
Deberías llamarlo en inputTypelugar de returnType, ¿no? ¿Dónde inputTypeestá el tipo del datacual pasa un parámetro a la callbackfunción?
ChrisW
Sí @ChrisW tienes razón, inputType tiene más sentido. ¡Gracias!
Krishna Ganeriwal
2

En TS podemos escribir funciones de la siguiente manera:

Tipos de funciones / firmas

Esto se usa para implementaciones reales de funciones / métodos, tiene la siguiente sintaxis:

(arg1: Arg1type, arg2: Arg2type) : ReturnType

Ejemplo:

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

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

}

Tipo de función Literales

Los literales de tipo de función son otra forma de declarar el tipo de una función. Por lo general, se aplican en la firma de la función de una función de orden superior. Una función de orden superior es una función que acepta funciones como parámetros o que devuelve una función. Tiene la siguiente sintaxis:

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

Ejemplo:

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

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

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

}
Willem van der Veen
fuente
1

Si define el tipo de función primero, se vería así

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

Sin el tipo de función utilizando la sintaxis de propiedad simple, sería:

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 lo desea mediante el uso de una función de interfaz como delegados genéricos de C # sería:

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
fuente
0

Además de lo que dijo otro, un problema común es declarar los tipos de la misma función que está sobrecargada. El caso típico es el método EventEmitter on () que aceptará múltiples tipos de oyentes. Puede ocurrir algo similar al trabajar con acciones redux, y allí usa el tipo de acción como literal para marcar la sobrecarga. En el caso de EventEmitters, usa el tipo de nombre de evento literal:

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
fuente