Как применить метод различающего объединения к сигнатурам функций?

#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.

Геттер может быть описан как сигнатура функции.

Я хочу использовать эту подпись в двух местах:

  1. Сервис, который реализует геттер, и он хорошо работает.
  2. Служба, которая вызывает геттера, и здесь у меня проблема с дискриминационным союзом.

Код:

 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. Я применил ваше исправление, и оно решает мою проблему, спасибо)