打字稿在编译时减去两个数字 [英] Typescript subtract two numbers at compile time

查看:28
本文介绍了打字稿在编译时减去两个数字的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要一个实用程序类型 Subtract 其中 AB 是数字.例如:

I need an utility type Subtract<A, B> where A and B are numbers. For example:

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 的参数总是数字文字或编译时常量.我不需要

Arguments to the Subtract<A, B> are always number literals or compile time constants. I do not need

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

编辑的问题

好吧,我认为上面的文字可能是XY问题,所以我想解释为什么我需要减法.我有一个多维数组,它具有 Dims 维度.当一个 slice 方法被调用时,它的维度会减少.

Edited question

Ok, I think that above text probably is the XY problem, so I want to explain why I need subtraction. I have a multidimensional array, which has Dims dimensions. When a slice method is called, its dimensions are reduced.

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 泛型如何取决于传递给 slice() 的参数数量.

You can see how Dims generic depends on number of arguments passed to slice().

如果Subtract类型很难,是否可以递减类型?因此,我可以执行以下操作:

If it is hard to make Subtract<A, B> type, is it possible to decrement type? So, I can do the following:

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

推荐答案

TypeScript 不支持编译时算术.但是,可以强制使用数组执行类似的操作,但您必须定义自己的方法算术.我会提前警告你,这绝对是可怕的.

TypeScript doesn't support compile-time arithmetic. However, it can be coerced to do something sort of similar using arrays, but you have to define your own method arithmetic. I'll warn you up front that it's absolutely terrible.

首先为数组操作定义一些基本类型:

Start with defining a few fundamental types for array manipulation:

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'].

These give you some power of array types, for example Tail<['foo', 'bar']> gives you ['bar'] and Cons<'foo', ['bar']> gives you ['foo', 'bar'].

现在您可以使用基于数组的数字(而不是 number)来定义一些算术概念:

Now you can define some arithmetic concepts using array-based numerals (not number):

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

所以数字 1 在这个系统中表示为 [void],2 表示为 [void, void] 等等.我们可以将加法和减法定义为:

So the numeral 1 would be represented in this system as [void], 2 is [void, void] and so on. We can define addition and subtraction as:

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];

如果你有决心,你也可以用类似的方式定义乘法和除法运算符.但就目前而言,这足以用作基本的算术系统.例如:

If you're determined, you can also define multiplication and division operators in a similar way. But for now, this is good enough to use as a basic system of arithmetic. For example:

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 常量来回转换.

Define a few other utility methods to convert back and forth from number constants.

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;

现在你可以像这样使用它们

And now you can use them like this

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 中递归类型的限制).但您真的应该在生产系统中执行此操作吗?

All of this was to show how powerful TypeScript's type system is, and just how far you can push it to do things it wasn't really designed for. It also only seems to reliably work up to N<23> or so (probably due to limits on recursive types within TypeScript). But should you actually do this in a production system?

当然,这种类型的滥用有点有趣(至少对我来说),但它far太复杂了,far太容易犯一些简单的错误极难调试.我强烈建议您对常量类型进行硬编码(const one: 1),或者按照评论的建议重新思考您的设计.

Sure, this sort of type abuse is kind of amusing (at least to me), but it's far too complex and far too easy to make simple mistakes that are extremely difficult debug. I highly recommend just hard-coding your constant types (const one: 1) or as the comments suggest, rethinking your design.

对于更新的问题,如果 Tensor 类型可以像上面的 Tail 一样轻松地减少(考虑到它是一个接口,这是值得怀疑的),你可以做这样的事情:

For the updated question, if the Tensor type can be easily reduced in the same way Tail does above (which is doubtful given that it's an interface), you could do something like this:

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>>;
}

然而,由于张量往往只有几个维度,我认为仅在用户最可能需要担心的少数情况下进行编码就足够了:

However, since tensors tend to only have a few dimensions, I think it's sufficient just to code in a handful of cases the user will most likely need to worry about:

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"代码,但是维护这段代码的成本可能仍然低于维护我上面描述的自定义算术系统的成本.

I know that this leads to some pretty 'WET' code, but the cost of maintaining this code is still probably less than the cost of maintaining a custom arithmetic system like what I described above.

请注意,官方 TypeScript 声明文件经常使用类似这样的模式(参见 lib.esnext.array.d.ts).强类型定义仅涵盖最常见的用例.对于任何其他用例,用户应在适当的情况下提供类型注释/断言.

Note that official TypeScript declaration files often use patterns a bit like this (see lib.esnext.array.d.ts). Only the most common use cases are covered with strongly typed definitions. For any other use cases, the user is expected to provide type annotations/assertions where appropriate.

这篇关于打字稿在编译时减去两个数字的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆