Typescript вычитает два числа во время компиляции

#typescript #typescript-generics

#typescript #typescript-generics

Вопрос:

Оригинальный вопрос

Мне нужен тип утилиты, Subtract<A, B> где A и B — числа. Например:

 type Subtract<A extends number, B extends number> = /* implementation */ 
const one: Subtract<2, 1> = 1
const two: Subtract<4, 2> = 2
const error: Subtract<2, 1> = 123 // Error here: 123 is not assignable to type '1'.
  

Аргументами Subtract<A, B> всегда являются числовые литералы или константы времени компиляции. Мне не нужно

 let foo: Subtract<number, number> // 'foo' may have 'number' type.
  

Отредактированный вопрос

Хорошо, я думаю, что приведенный выше текст, вероятно, является проблемой XY, поэтому я хочу объяснить, почему мне нужно вычитание. У меня есть многомерный массив, который имеет Dims размеры. При вызове slice метода его размеры уменьшаются.

 interface Tensor<Dimns extends number> {
    // Here `I` type is a type of indeces 
    // and its 'length' is how many dimensions 
    // are subtracted from source array.
    slice<I extends Array<[number, number]>>(...indeces: I): Tensor<Dimns - I['length']>
                                               // here I need to subtract ^ 
}
  

Примеры:

 declare const arr: Tensor<4, number>
arr.slice([0, 1])               // has to be 3d array
arr.slice([0, 1], [0, 2])       // has to be 2d array
arr.slice([0, 1]).slice([0, 2]) // has to be 2d array
  

Вы можете видеть, как Dims generic зависит от количества переданных аргументов slice() .

Если трудно создать Subtract<A, B> тип, возможно ли уменьшить тип? Итак, я могу сделать следующее:

 interface Tensor<Dimns extends number> {
    // Here `Decrement<A>` reduces the number of dimensions by 1.
    slice(start: number, end: number): Tensor<Decrement<Dimns>>
}
  

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

1. Типы работают не так. Вам понадобится другой дизайн.

Ответ №1:

TypeScript не поддерживает арифметику во время компиляции. Тем не менее, его можно принудительно выполнить нечто подобное с использованием массивов, но вы должны определить свой собственный метод arithmetics . Я заранее предупреждаю вас, что это абсолютно ужасно.

Начните с определения нескольких основных типов для работы с массивами:

 type Tail<T> = T extends Array<any> ? ((...x: T) => void) extends ((h: any, ...t: infer I) => void) ? I : [] : unknown;
type Cons<A, T> = T extends Array<any> ? ((a: A, ...t: T) => void) extends ((...i: infer I) => void) ? I : unknown : never;
  

Это дает вам некоторую возможность использовать типы массивов, например, Tail<['foo', 'bar']> дает вам ['bar'] и Cons<'foo', ['bar']> дает вам ['foo', 'bar'] .

Теперь вы можете определить некоторые арифметические понятия, используя цифры на основе массива (не number ):

 type Zero = [];
type Inc<T> = Cons<void, T>;
type Dec<T> = Tail<T>;
  

Таким образом, цифра 1 будет представлена в этой системе как [void] , 2 — это [void, void] и так далее. Мы можем определить сложение и вычитание как:

 type Add<A, B> = { 0: A, 1: Add<Inc<A>, Dec<B>> }[Zero extends B ? 0 : 1];
type Sub<A, B> = { 0: A, 1: Sub<Dec<A>, Dec<B>> }[Zero extends B ? 0 : 1];
  

Если вы настроены решительно, вы также можете определить операторы умножения и деления аналогичным образом. Но на данный момент этого достаточно, чтобы использовать в качестве базовой системы арифметики. Например:

 type One = Inc<Zero>;                    // [void]
type Two = Inc<One>;                     // [void, void]
type Three = Add<One, Two>;              // [void, void, void]
type Four = Sub<Add<Three, Three>, Two>; // [void, void, void, void]
  

Определите несколько других служебных методов для обратного преобразования number констант.

 type N<A extends number, T = Zero> = { 0: T, 1: N<A, Inc<T>> }[V<T> extends A ? 0 : 1];
type V<T> = T extends { length: number } ? T['length'] : unknown;
  

И теперь вы можете использовать их следующим образом

 const one: V<Sub<N<2>, N<1>>> = 1;
const two: V<Sub<N<4>, N<2>>> = 2;
const error: V<Sub<N<2>, N<1>>> = 123; // Type '123' is not assignable to type '1'.
  

Все это было сделано для того, чтобы показать, насколько мощной является система типов TypeScript и насколько далеко вы можете подтолкнуть ее к выполнению того, для чего она на самом деле не предназначалась. Также кажется, что он надежно работает только до N<23> или около того (вероятно, из-за ограничений на рекурсивные типы в TypeScript). Но стоит ли на самом деле делать это в производственной системе?

Нет!

Конечно, такого рода злоупотребление типом в некотором роде забавно (по крайней мере, для меня), но это намного слишком сложно и намного слишком легко допустить простые ошибки, которые чрезвычайно трудно отлаживать. Я настоятельно рекомендую просто жестко запрограммировать ваши постоянные типы ( const one: 1 ) или, как предполагают комментарии, переосмыслить ваш дизайн.


Что касается обновленного вопроса, если Tensor тип можно легко уменьшить таким же образом, Tail как указано выше (что сомнительно, учитывая, что это интерфейс), вы могли бы сделать что-то вроде этого:

 type Reduced<T extends Tensor<number>> = T extends Tensor<infer N> ? /* construct Tensor<N-1> from Tensor<N> */ : Tensor<number>;

interface Tensor<Dimns extends number> {
  slice(start: number, end: number): Reduced<Tensor<Dimns>>;
}
  

Однако, поскольку тензоры, как правило, имеют только несколько измерений, я думаю, что достаточно просто закодировать в нескольких случаях, о которых пользователю, скорее всего, придется беспокоиться:

 type SliceIndeces<N extends number> = number[] amp; { length: N };
interface Tensor<Dims extends number> {
  slice(this: Tensor<5>, ...indeces: SliceIndeces<1>): Tensor<4>;
  slice(this: Tensor<5>, ...indeces: SliceIndeces<2>): Tensor<3>;
  slice(this: Tensor<5>, ...indeces: SliceIndeces<3>): Tensor<2>;
  slice(this: Tensor<5>, ...indeces: SliceIndeces<2>): Tensor<1>;
  slice(this: Tensor<4>, ...indeces: SliceIndeces<1>): Tensor<3>;
  slice(this: Tensor<4>, ...indeces: SliceIndeces<2>): Tensor<2>;
  slice(this: Tensor<4>, ...indeces: SliceIndeces<3>): Tensor<1>;
  slice(this: Tensor<3>, ...indeces: SliceIndeces<1>): Tensor<2>;
  slice(this: Tensor<3>, ...indeces: SliceIndeces<2>): Tensor<1>;
  slice(this: Tensor<2>, ...indeces: SliceIndeces<1>): Tensor<1>;
  slice(...indeces:number[]): Tensor<number>;
}

const t5: Tensor<5> = ...
const t3 = t5.slice(0, 5); // inferred type is Tensor<3>
  

Я знаю, что это приводит к довольно «мокрому» коду, но стоимость поддержки этого кода все еще, вероятно, меньше, чем стоимость поддержки пользовательской арифметической системы, подобной той, что я описал выше.

Обратите внимание, что официальные файлы объявлений TypeScript часто используют шаблоны, немного похожие на этот (см. lib.esnext.array.d.ts ). Строго типизированными определениями охвачены только наиболее распространенные варианты использования. Для любых других вариантов использования ожидается, что пользователь предоставит аннотации типа / утверждения, где это уместно.

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

1. Отлично сделано :). Не то чтобы меня немного не вырвало, когда я это увидел … 😉

2. Спасибо за ваш ответ. Я обновил свой вопрос и объяснил происхождение проблемы, возможно, это поможет лучше понять, чего я хочу

3. Связанная проблема: Microsoft / TypeScript#26223