Возможны ли строго типизированные функции как параметры в TypeScript?

560

В TypeScript я могу объявить параметр функции как тип Function. Есть ли «безопасный для типов» способ сделать это, что мне не хватает? Например, рассмотрим это:

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

Функция обратного вызова save не является безопасной для типов, я даю ей функцию обратного вызова, где параметром функции является строка, но я передаю ей число и компилирую без ошибок. Можно ли сделать параметр результата в функции сохранения типа безопасной?

TL; версия DR: есть ли в TypeScript эквивалент делегата .NET?

vcsjones
источник

Ответы:

805

Конечно. Функция , в типе состоит из типов аргумента и типа возвращаемого значения. Здесь мы указываем, что callbackтип параметра должен быть «функцией, которая принимает число и возвращает тип 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

Если вы хотите, вы можете определить псевдоним типа, чтобы инкапсулировать это:

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

class Foo {
    // Equivalent
    save(callback: NumberCallback) : void {
        callback(42);
    }
}
Райан Кавано
источник
6
(n: number) => anyзначит какая-то функция подписи?
Никк Вонг
16
@nikkwong это означает, что функция принимает один параметр (a number), но тип возвращаемого значения вообще не ограничен (может быть любым значением или даже void)
Даниэль Эрвикер,
16
Какой смысл nв этом синтаксисе? Разве одних типов ввода и вывода будет недостаточно?
Юйхуань Цзян
4
Одним из побочных эффектов между использованием встроенных функций и именованных функций (ответ ниже против этого ответа) является то, что переменная this не определена с именованной функцией, тогда как она определена внутри встроенной функции. Не удивительно для JavaScript-кодеров, но определенно не очевидно для других фонов кодирования.
Стевко
3
@YuhuanJiang Это сообщение может представлять интерес для Вас
Ophidian
93

Вот TypeScript-эквиваленты некоторых распространенных делегатов .NET:

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

interface Func<T,TResult>
{
    (item: T): TResult;
}
Дрю Ноакс
источник
2
Вероятно, полезно взглянуть на это, но было бы противодействовать использованию таких типов. В любом случае, они больше похожи на типы Java SAM, чем на делегаты C #. Конечно, это не так, и они эквивалентны форме псевдонима типа, которая более элегантна для функций
Алуан Хаддад
5
@AluanHaddad, не могли бы вы рассказать, почему вы думаете, что это антипаттерн?
Макс Р Маккарти
8
Причина в том, что TypeScript имеет краткий синтаксис буквенного типа функции, который устраняет необходимость в таких интерфейсах. В C # делегаты являются номинальными, но Actionи Funcделегаты, и делегаты устраняют большую часть потребности в конкретных типах делегатов и, что интересно, дают C # a подобие структурной типизации. Недостатком этих делегатов является то, что их имена не имеют никакого значения, но другие преимущества, как правило, перевешивают это. В TypeScript нам просто не нужны эти типы. Таким образом, анти-шаблон будет function map<T, U>(xs: T[], f: Func<T, U>). Предпочитаюfunction map<T, U>(xs: T[], f: (x: T) => U)
Алуан Хаддад
6
Это дело вкуса, поскольку это эквивалентные формы в языке, который не имеет типов времени выполнения. В настоящее время вы также можете использовать псевдонимы типов вместо интерфейсов.
Дрю Ноакс
18

Я понимаю, что этот пост старый, но есть более компактный подход, который немного отличается от того, о чем просили, но может быть очень полезной альтернативой. Вы можете существенно объявить функцию в линии при вызове метода ( Foo«ы save()в данном случае). Это будет выглядеть примерно так:

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()подход очень полезен для таких вещей, как сетевые вызовы, которые могут быть успешными или неудачными. Опять же, предполагая пример сетевого вызова, когда он multipleCallbacks()вызывается, поведение как для успеха, так и для отказа может быть определено в одном месте, что обеспечивает большую ясность для будущих читателей кода.

В целом, по моему опыту, этот подход позволяет быть более кратким, менее беспорядочным и большей ясностью в целом.

Всем удачи!

kbpontius
источник
16
type FunctionName = (n: inputType) => any;

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

Это, безусловно, согласуется с парадигмой функционального программирования.

Кришна Ганеривал
источник
6
Вы должны называть это, inputTypeа не returnType, не так ли? Где inputTypeтип, в dataкоторый вы передаете параметр callbackфункции.
ChrisW
Да, Крис, вы правы, inputType имеет больше смысла. Спасибо!
Кришна Ганеривал
2

В TS мы можем вводить функции следующими способами:

Типы функций / подписи

Используется для реальных реализаций функций / методов, имеет следующий синтаксис:

(arg1: Arg1type, arg2: Arg2type) : ReturnType

Пример:

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

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

}

Тип функции Литералы

Литералы типа функции - это еще один способ объявить тип функции. Они обычно применяются в сигнатуре функции высшего порядка. Функция более высокого порядка - это функция, которая принимает функции в качестве параметров или возвращает функцию. Он имеет следующий синтаксис:

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

Пример:

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

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

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

}
Виллем ван дер Веен
источник
1

Если вы сначала определите тип функции, то это будет выглядеть так

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

Без типа функции с использованием синтаксиса простого свойства это будет:

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

Если вы хотите использовать интерфейсную функцию, такую ​​как обобщенные делегаты c #, это будет:

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
источник
0

Помимо того, что сказали другие, общая проблема заключается в объявлении типов той же функции, которая перегружена. Типичным случаем является метод EventEmitter on (), который будет принимать несколько типов слушателей. Подобное может произойти при работе с избыточными действиями - и там вы используете тип действия как литерал для обозначения перегрузки. В случае EventEmitters вы используете литеральный тип имени события:

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
источник