Машинопись вычесть два числа во время компиляции - PullRequest
0 голосов
/ 04 марта 2019

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

Мне нужен тип утилиты 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 Ответ

0 голосов
/ 04 марта 2019

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

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

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[] & { 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>

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

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

...