Funções fortemente tipadas como parâmetros são possíveis no TypeScript?

559

No TypeScript, posso declarar um parâmetro de uma função como um tipo Function. Existe uma maneira "segura de digitar" de fazer isso que estou perdendo? Por exemplo, considere isso:

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

O retorno de chamada para salvar não é do tipo seguro; estou fornecendo uma função de retorno de chamada em que o parâmetro da função é uma cadeia de caracteres, mas estou passando um número para ele e compilando sem erros. Posso definir o parâmetro result em salvar uma função de segurança de tipo?

Versão TL; DR: existe o equivalente a um delegado .NET no TypeScript?

vcsjones
fonte

Respostas:

805

Certo. O tipo de uma função consiste nos tipos de seu argumento e seu tipo de retorno. Aqui, especificamos que o callbacktipo do parâmetro deve ser "função que aceita um número e retorna o 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

Se desejar, você pode definir um alias de tipo para encapsular isso:

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

class Foo {
    // Equivalent
    save(callback: NumberCallback) : void {
        callback(42);
    }
}
Ryan Cavanaugh
fonte
6
(n: number) => anysignifica alguma assinatura de função?
Nikk wong
16
@nikkwong significa que a função recebe um parâmetro (a number), mas o tipo de retorno não está restrito a todos (poderia ser qualquer valor, ou mesmo void)
Daniel Earwicker
16
Qual é o objetivo ndessa sintaxe? Os tipos de entrada e saída não seriam suficientes?
Yuhuan Jiang
4
Um efeito colateral entre o uso de funções embutidas versus funções nomeadas (resposta abaixo vs esta resposta) é a variável "this" não definida com a função nomeada, enquanto é definida na função embutida. Não é surpresa para os codificadores JavaScript, mas definitivamente não é óbvio para outros antecedentes de codificação.
Stevko
3
@YuhuanJiang Este post pode ser do seu interesse
Ophidian
93

Aqui estão os equivalentes TypeScript de alguns delegados .NET comuns:

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

interface Func<T,TResult>
{
    (item: T): TResult;
}
Drew Noakes
fonte
2
Provavelmente útil, mas seria um anti-padrão usar esses tipos. Enfim, eles se parecem mais com os tipos Java SAM do que com os representantes de C #. É claro que eles não são e eles são equivalentes à forma apelido tipo que é apenas mais elegante para funções
Aluan Haddad
5
@AluanHaddad, você poderia explicar por que você acha isso um antipadrão?
Max R McCarty
8
O motivo é que o TypeScript possui uma sintaxe literal concisa de tipo de função que evita a necessidade de tais interfaces. Em C #, os delegados são nominais, mas os delegados Actione Funcevitam a maior parte da necessidade de tipos de delegados específicos e, curiosamente, fornecem ao C # uma aparência de tipagem estrutural. A desvantagem desses delegados é que seus nomes não transmitem significado, mas as outras vantagens geralmente superam isso. No TypeScript, simplesmente não precisamos desses tipos. Então o anti-padrão seria function map<T, U>(xs: T[], f: Func<T, U>). Preferfunction map<T, U>(xs: T[], f: (x: T) => U)
Aluan Haddad
6
É uma questão de gosto, pois essas são formas equivalentes em um idioma que não possui tipos de tempo de execução. Atualmente, você também pode usar aliases de tipo em vez de interfaces.
Drew Noakes
18

Sei que este post é antigo, mas há uma abordagem mais compacta que é um pouco diferente do que foi solicitado, mas pode ser uma alternativa muito útil. Você pode essencialmente declarar a função in-line ao chamar o método ( Foo's save(), neste caso). Seria algo como isto:

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

A multipleCallback()abordagem é muito útil para coisas como chamadas de rede que podem ter sucesso ou falhar. Novamente, assumindo um exemplo de chamada de rede, quando multipleCallbacks()chamado, o comportamento para um sucesso e uma falha pode ser definido em um local, o que se presta a uma maior clareza para futuros leitores de código.

Geralmente, na minha experiência, essa abordagem se presta a ser mais concisa, menos confusa e com maior clareza em geral.

Boa sorte a todos!

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

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

Isso certamente está alinhado com o paradigma de programação funcional.

Krishna Ganeriwal
fonte
6
Você deveria chamá-lo em inputTypevez de returnType, não deveria? Onde inputTypeé o tipo do dataqual você passa um parâmetro para a callbackfunção.
31418 ChrisW
Sim @ChrisW você está certo, inputType faz mais sentido. Obrigado!
Krishna Ganeriwal 04/07/19
2

No TS, podemos digitar funções das seguintes maneiras:

Tipos de funções / assinaturas

Isso é usado para implementações reais de funções / métodos e possui a seguinte sintaxe:

(arg1: Arg1type, arg2: Arg2type) : ReturnType

Exemplo:

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

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

}

Tipo de Função Literais

Literais de tipo de função são outra maneira de declarar o tipo de uma função. Eles geralmente são aplicados na assinatura da função de uma função de ordem superior. Uma função de ordem superior é uma função que aceita funções como parâmetros ou que retorna uma função. Possui a seguinte sintaxe:

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

Exemplo:

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

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

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

}
Willem van der Veen
fonte
1

Se você definir o tipo de função primeiro, pareceria

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

Sem o tipo de função usando a sintaxe de propriedade simples, seria:

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

Se você deseja usar uma função de interface como delegados genéricos do c #, seria:

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

Além do que outros disseram, um problema comum é declarar os tipos da mesma função que estão sobrecarregados. O caso típico é o método EventEmitter on () que aceita vários tipos de ouvintes. Semelhante pode acontecer Ao trabalhar com ações redux - e você usa o tipo de ação como literal para marcar a sobrecarga. No caso de EventEmitters, você usa o tipo literal do nome do evento:

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
fonte