Функции более высокого порядка в машинописном тексте?

#typescript #generics #typescript-generics

Вопрос:

Пожалуйста, рассмотрите следующий псевдокод, пытающийся определить функцию типа более высокого порядка с параметром, типизированным функцией M<?> :

 type HigherOrderTypeFn<T, M<?>> = T extends (...)
  ? M<T>
  : never;
 

M<?> синтаксически неверный текст, но объявление подписи типа как HigherOrderTypeFn<T, M> приводит к ошибке Type 'M' is not generic. ts(2315) во второй строке.

Правильно ли я предполагаю, что такой тип в настоящее время не представлен в TS?

Ответ №1:

Вы правы, в настоящее время это невозможно представить в машинописном виде. Существует давний запрос на функцию open GitHub, microsoft/TypeScript#1213, который, вероятно, должен называться что-то вроде «поддержка типов с более высоким уровнем сложности», но в настоящее время имеет заголовок «Разрешить классам быть параметрическими в других параметрических классах».

В обсуждении есть некоторые идеи о том, как имитировать такие типы более высокого уровня на текущем языке (см. Этот комментарий для конкретного примера), но, на мой взгляд, они, вероятно, не относятся к производственному коду. Если у вас есть какая-то конкретная структура, которую вы собираетесь реализовать, возможно, можно предложить что — то подходящее.

Но в любом случае, если вы хотите увеличить вероятность (вероятно, незначительно, к сожалению), что это когда-либо произойдет, вы можете обратиться к этой проблеме и описать ее 👍 и/или описать свой вариант использования, если вы считаете, что он особенно убедителен по сравнению с тем, что уже есть. Хорошо, надеюсь, это поможет; удачи!

Комментарии:

1. Спасибо @jcalz за прояснение этого вопроса! Это был эксперимент с моей стороны (попытка сократить набор текста), но я согласен, что мне будет лучше реализовать это более лаконичным (хотя и более избыточным) способом в производстве. Приятно видеть, однако, что такая конструкция выполнима с текущей семантикой TS.

2. Это обсуждение заставляет меня думать, что будущие языки программирования будут очень похожи на Хаскелл…

Ответ №2:

Для вас и других, кто ищет обходной путь, вы можете попробовать простую идею, основанную на заполнителях (см. Этот комментарий в обсуждении, упомянутом jcalz).:

 type Placeholder = {'aUniqueKey': unknown};
type Replace<T, X, Y> = {
  [k in keyof T]: T[k] extends X ? Y : T[k];
};
 

Итак, ваша функция будет выглядеть следующим образом:

 type HigherOrderTypeFn<T, M> = T extends (...) ? Replace<M, Placeholder, T> : never;
 

и называться, например, так:

 type M<U> = U[];
type X = HigherOrderTypeFn<number, M<Placeholder>> // is number[] (if ... is number)
 

Ответ №3:

Для людей, столкнувшихся с этим, на сервере TypeScript discord есть такой хороший пример:

 export interface Hkt<I = unknown, O = unknown> {
  [Hkt.isHkt]: never,
  [Hkt.input]: I,
  [Hkt.output]: O,
}

export declare namespace Hkt {
  const isHkt: unique symbol
  const input: unique symbol
  const output: unique symbol

  type Input<T extends Hkt<any, any>> =
    T[typeof Hkt.input]

  type Output<T extends Hkt<any, any>, I extends Input<T>> =
    (T amp; { [input]: I })[typeof output]

  interface Compose<O, A extends Hkt<any, O>, B extends Hkt<any, Input<A>>> extends Hkt<Input<B>, O>{
    [output]: Output<A, Output<B, Input<this>>>,
  }

  interface Constant<T, I = unknown> extends Hkt<I, T> {}
}
 

Который может быть использован следующим образом. Приведенный ниже фрагмент определяет a SetFactory , где вы указываете тип, желаемый тип набора при создании фабрики, например typeof FooSet , или typeof BarSet . typeof FooSet является конструктором для a FooSet и похож на более высокий тип, тип конструктора принимает любой T и возвращает a FooSet<T> . SetFactory Содержит несколько методов , таких как createNumberSet , который возвращает новый набор данного типа с параметрами типа, установленными на number .

 interface FooSetHkt extends Hkt<unknown, FooSet<any>> {
    [Hkt.output]: FooSet<Hkt.Input<this>>
}
class FooSet<T> extends Set<T> {
    foo() {} 
    static hkt: FooSetHkt;
}

interface BarSetHkt extends Hkt<unknown, BarSet<any>> {
    [Hkt.output]: BarSet<Hkt.Input<this>>;
}
class BarSet<T> extends Set<T> { 
    bar() {} 
    static hkt: BarSetHkt;
}

class SetFactory<Cons extends {
    new <T>(): Hkt.Output<Cons["hkt"], T>;
    hkt: Hkt<unknown, Set<any>>;
}> {
    constructor(private Ctr: Cons) {}
    createNumberSet() { return new this.Ctr<number>(); }
    createStringSet() { return new this.Ctr<string>(); }
}

// SetFactory<typeof FooSet>
const fooFactory = new SetFactory(FooSet);
// SetFactory<typeof BarSet>
const barFactory = new SetFactory(BarSet);

// FooSet<number>
fooFactory.createNumberSet();
// FooSet<string>
fooFactory.createStringSet();

// BarSet<number>
barFactory.createNumberSet();
// BarSet<string>
barFactory.createStringSet();
 

Краткое объяснение того, как это работает (с FooSet примером и number в качестве примера):

  • Основной тип, который нужно понять, таков Hkt.Output<Const["hkt"], T> . С заменой наших типов примеров это становится Hkt.Output<(typeof FooSet)["hkt"], number> . Магия теперь включает в себя превращение этого в FooSet<number>
  • Сначала мы решаем (typeof FooSet)["hkt"] FooSetHkt это сделать . Большая часть магии кроется здесь в хранении информации о том, как создать a FooSet в статическом hkt свойстве FooSet . Вам нужно сделать это для каждого поддерживаемого класса.
  • Теперь у нас есть Hkt.Output<FooSetHkt, number> . Разрешая Hkt.Output псевдоним типа, мы получаем (FooSetHkt amp; { [Hkt.input]: number })[typeof Hkt.output] . Уникальные символы Hkt.input / Hkt.output справка для создания уникальных свойств, но мы могли бы также использовать уникальные строковые константы.
  • Теперь нам нужно получить доступ к Hkt.output собственности FooSetHkt . Это отличается для каждого класса и содержит подробные сведения о том, как создать конкретный тип с аргументом типа. FooSetHkt определяет свойство вывода, которое должно иметь тип FooSet<Hkt.Input<this>> .
  • Наконец, Hkt.Input<this> просто присоединяется к Hkt.input свойству FooSetHkt . Это разрешило бы unknown , но , используя пересечение FooSetHkt amp; { [Hkt.input]: number } , мы можем изменить Hkt.input свойство на number . И поэтому, если мы достигли нашей цели, Hkt.Input<this> решаемся number и FooSet<Hkt.Input<this>> решаемся FooSet<number> .

Для примера из вопроса, Hkt.Output по сути, это то, о чем просили, просто с измененными параметрами типа:

 interface List<T> {}
interface ListHkt extends Hkt<unknown, List<any>> {
    [Hkt.output]: List<Hkt.Input<this>>
}
type HigherOrderTypeFn<T, M extends Hkt> = Hkt.Output<M, T>;
// Gives you List<number>
type X = HigherOrderTypeFn<number, ListHkt>;