#typescript #typescript-generics #discriminated-union
Вопрос:
Я хочу применить метод различающего объединения к сигнатурам функций.
Пример кода
type fun1 = (opts: {type: 'a', value: string}) => string;
type fun2 = (opts: {type: 'b', value: number}) => number;
type anyFun = fun1 | fun2;
class Clazz {
innerMethod<T extends anyFun>(opts: Parameters<T>[0]): ReturnType<T> {
return 123 as ReturnType<T>;
}
otherMethod() {
// Case 1 - FAILED
// expected result type: string
// actual result type: string | number
const result1 = this.innerMethod({type: 'a', value: 'str'});
// Case 2 - OK
// expected result type = actual type = string
const result2 = this.innerMethod<fun1>({type: 'a', value: 'str'});
// Case 3 - OK
// TS error that fun2 is incompatible with type: 'a'
const result3 = this.innerMethod<fun2>({type: 'a', value: 'str'});
}
}
Я хочу написать код в случае 1 способом без явного объявления типа, но вывод типа TypeScript в этом случае возвращает неправильный тип.
Как можно написать код таким образом, используя сигнатуры функций? Или единственный способ-изменить типы сигнатур функций?
UPD. Немного больше информации о том, какую проблему я хочу решить.
У меня есть фасад, который охватывает набор услуг.
Каждая служба имеет API, и часть этого API является синхронными получателями.
Служба может запрашивать информацию у другой службы через getter.
Геттер может быть описан как сигнатура функции.
Я хочу использовать эту подпись в двух местах:
- Сервис, который реализует геттер, и он хорошо работает.
- Служба, которая вызывает геттера, и здесь у меня проблема с дискриминационным союзом.
Код:
type Fork = number;
type Spoon = string;
type GetFork = (arg: {type: 'fork', id: number}) => Fork;
type GetSpoon = (arg: {type: 'spoon', id: number}) => Spoon;
interface Facade {
constructor(services: AbstractService[]);
// routes request to service which can execute getter
get(request): any;
}
class AbstractService<NeedGetters extends (arg: any) => any = never> {
facade!: Facade;
init(facade: Facade) {
this.facade = facade;
}
getFromFacade<T extends NeedGetters>(opts: Parameters<T>[0]): ReturnType<T> {
return this.facade.get(opts);
}
}
interface ForksService extends AbstractService {
getFork({type}: Parameters<GetFork>[0]): ReturnType<GetFork>;
}
interface SpoonsService extends AbstractService {
getSpoon({type}: Parameters<GetSpoon>[0]): ReturnType<GetSpoon>;
}
class StuffService extends AbstractService<GetFork | GetSpoon> {
someMethod() {
// TS infered type string | number
const fork = this.getFromFacade({type: 'fork', id: 25});
// TS infered type string | number
const spoon = this.getFromFacade({type: 'spoon', id: 36});
}
}
Ответ №1:
Я не уверен, почему это представлено в виде сигнатур функций, когда, похоже, функции такого типа не существует, но если бы я хотел написать версию innerMethod()
, допускающую вывод типов, я бы сделал что-то вроде этого:
innerMethod<P extends Parameters<AnyFun>[0]>(
opts: P
): ReturnType<Extract<AnyFun, (opts: P) => any>> {
return 123 as any;
}
Проблема с вашей версией заключается в том, что компилятор не может достоверно вывести T extends AnyFun
значение типа Parameters<T>[0]
. Компилятор не знает, как «перепроектировать» правильный T
тип из этого. Наиболее надежная форма вывода типа-дать компилятору значение типа, который вы хотите вывести, а затем вычислить другие типы на его основе.
В приведенном выше примере тип P
-это тип передаваемого opts
параметра. Мы ограничиваем его Parameters<AnyFun>[0]
, что является
{
type: "a";
value: string;
} | {
type: "b";
value: number;
}
Один из них у нас есть P
, мы можем использовать Extract
тип утилиты, чтобы получить элемент AnyFun
, первый параметр которого имеет тип P
, а затем использовать ReturnType
его. Вы можете видеть, что теперь это работает:
otherMethod() {
const result1 = this.innerMethod({ type: 'a', value: 'str' }); // string
result1.toUpperCase(); // okay
}
Но, как я уже сказал, мне кажется странным представлять такое сопоставление типов в терминах функций. В отсутствие смягчающей информации о вашем случае использования я был бы склонен переписать ваш код следующим образом:
type IOMapping = { a: string, b: number }
innerMethod<K extends keyof IOMapping>(
opts: { type: K, value: IOMapping[K] }
): IOMapping[K] {
return 123 as any;
}
который ведет себя аналогично с точки зрения вывода типов, но требует значительно меньшего количества манипуляций с типами.
Комментарии:
1. Я добавил больше информации о проблеме, которую хочу решить.
2. Первая половина моего ответа, переведенная на этот код, выглядит так , как я думаю, который выводит
fork
иspoon
вводит по желанию.3. Я применил ваше исправление, и оно решает мою проблему, спасибо)