Czy funkcje o silnym typie są parametrami możliwymi w TypeScript?

558

W TypeScript mogę zadeklarować parametr funkcji jako typ funkcji. Czy istnieje sposób „bezpieczny dla typu”, którego mi brakuje? Rozważmy na przykład:

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

Funkcja callback save nie jest bezpieczna dla typu, nadaję jej funkcję callback, w której parametrem funkcji jest ciąg znaków, ale przekazuję jej liczbę i kompiluję bez błędów. Czy mogę ustawić parametr wynikowy w funkcji bezpiecznej dla typu?

Wersja TL; DR: czy w TypeScript istnieje odpowiednik delegata .NET?

vcsjones
źródło

Odpowiedzi:

804

Pewnie. Funkcja jest typu składa się z rodzajów jego argumentacji i jego typ zwracany. W tym miejscu określamy, że callbacktyp parametru musi być „funkcją, która przyjmuje liczbę i zwraca typ 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

Jeśli chcesz, możesz zdefiniować alias typu, aby to zawrzeć:

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

class Foo {
    // Equivalent
    save(callback: NumberCallback) : void {
        callback(42);
    }
}
Ryan Cavanaugh
źródło
6
(n: number) => anyoznacza jakąś sygnaturę funkcji?
nikk wong
16
@nikkwong oznacza, że ​​funkcja przyjmuje jeden parametr (a number), ale typ zwrotu nie jest w ogóle ograniczony (może to być dowolna wartość, a nawet void)
Daniel Earwicker
16
Jaki jest sens ntej składni? Czy same typy danych wejściowych i wyjściowych nie byłyby wystarczające?
Yuhuan Jiang
4
Jednym efektem ubocznym między używaniem funkcji wbudowanych a funkcjami nazwanymi (odpowiedź poniżej vs ta odpowiedź) jest to, że zmienna „ta” jest niezdefiniowana dla funkcji nazwanej, podczas gdy jest zdefiniowana w funkcji wbudowanej. Nie jest zaskoczeniem dla programistów JavaScript, ale zdecydowanie nie jest oczywiste dla innych środowisk kodowania.
Stevko
3
@YuhuanJiang Ten post może Cię zainteresować
Ophidian
93

Oto odpowiedniki TypeScript niektórych popularnych delegatów platformy .NET:

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

interface Func<T,TResult>
{
    (item: T): TResult;
}
Drew Noakes
źródło
2
Prawdopodobnie warto na to spojrzeć, ale używanie takich typów byłoby anty-wzorzec. W każdym razie wyglądają bardziej jak typy Java SAM niż delegaci C #. Oczywiście, że nie są i są równoważne z formą aliasu typu, która jest po prostu bardziej elegancka dla funkcji
Aluan Haddad
5
@ AlluanHaddad mógłbyś wyjaśnić, dlaczego uważasz, że to anty-wzór?
Max R McCarty,
8
Powodem jest to, że TypeScript ma zwięzłą składnię literalną typu funkcji, która eliminuje potrzebę takich interfejsów. W języku C # delegaty są nominalne, ale zarówno delegaci, jak Actioni Funcuczestnicy eliminują większość zapotrzebowania na określone typy delegatów i, co ciekawe, dają C # pozory typowego pisania strukturalnego. Minusem tych delegatów jest to, że ich nazwiska nie mają żadnego znaczenia, ale inne zalety na ogół przeważają nad tym. W TypeScript po prostu nie potrzebujemy tych typów. Tak więc byłby anty-wzór function map<T, U>(xs: T[], f: Func<T, U>). Wolęfunction map<T, U>(xs: T[], f: (x: T) => U)
Aluan Haddad
6
To kwestia gustu, ponieważ są to równoważne formy w języku, który nie ma typów wykonawczych. Obecnie można również używać aliasów typów zamiast interfejsów.
Drew Noakes
18

Zdaję sobie sprawę, że ten post jest stary, ale istnieje bardziej zwarte podejście, które nieco różni się od tego, o co pytano, ale może być bardzo pomocną alternatywą. Można zasadniczo zadeklarować funkcję w linii podczas wywoływania metody ( Foo„s save()w tym przypadku). Wyglądałoby to tak:

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

multipleCallback()Podejście jest bardzo przydatna dla rzeczy jak połączeń sieciowych, które może uda lub nie. Ponownie zakładając przykład wywołania sieciowego, gdy multipleCallbacks()jest wywoływany, zachowanie zarówno sukcesu, jak i niepowodzenia można zdefiniować w jednym miejscu, co zapewnia większą przejrzystość dla przyszłych czytników kodów.

Ogólnie z mojego doświadczenia wynika, że ​​takie podejście jest bardziej zwięzłe, mniej zagracone i ogólnie bardziej przejrzyste.

Powodzenia wszystkim!

kbpontius
źródło
16
type FunctionName = (n: inputType) => any;

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

Z pewnością jest to zgodne z paradygmatem programowania funkcjonalnego.

Krishna Ganeriwal
źródło
6
Powinieneś to inputTyperaczej nazwać returnType, prawda? Gdzie inputTypejest typ, dataktóry przekazujesz parametr do callbackfunkcji.
ChrisW,
Tak @Chris Masz rację, inputType ma większy sens. Dzięki!
Krishna Ganeriwal
2

W TS możemy wpisywać funkcje w następujący sposób:

Rodzaje funkcji / podpisy

Służy do rzeczywistych implementacji funkcji / metod i ma następującą składnię:

(arg1: Arg1type, arg2: Arg2type) : ReturnType

Przykład:

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

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

}

Literały typu funkcji

Literały typu funkcji to inny sposób deklarowania typu funkcji. Zazwyczaj są one stosowane w sygnaturze funkcji funkcji wyższego rzędu. Funkcja wyższego rzędu to funkcja, która przyjmuje funkcje jako parametry lub zwraca funkcję. Ma następującą składnię:

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

Przykład:

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

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

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

}
Willem van der Veen
źródło
1

Jeśli najpierw zdefiniujesz typ funkcji, będzie to wyglądać następująco

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

Bez typu funkcji przy użyciu zwykłej składni właściwości byłoby to:

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

Jeśli chcesz za pomocą funkcji interfejsu, takiej jak delegaty ogólne c #, byłoby to:

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
źródło
0

Oprócz tego, co powiedzieli inni, częstym problemem jest deklarowanie typów tej samej funkcji, która jest przeciążona. Typowym przypadkiem jest metoda EventEmitter on (), która akceptuje wiele rodzajów detektorów. Podobnie może się zdarzyć Podczas pracy z operacjami redux - i tam używasz typu akcji jako literału, aby oznaczyć przeciążenie, W przypadku EventEmitters, używasz typu literału nazwy zdarzenia:

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
rakbero
źródło