【问题标题】:How to declare a tuple type of size N (to use for vector class)?如何声明大小为 N 的元组类型(用于向量类)?
【发布时间】:2021-07-02 04:34:16
【问题描述】:

我正在尝试创建一个具有固定大小 N 的元组类型,所以我可以这样做:

let tuple: Tuple<string, 2> = ["a","b"]

其中“数字”是类型 T,“2”是大小 N。然后我想创建一个(数学)向量类,它实现元组并添加方法(例如向量加法),如下所示:

class Vector<N extends number> implements Tuple<number,N> {...}

我确实找到了很多解决方案(用于元组类型的实现),但都有一些问题。我找到的最简单的解决方案 (here) 是使用这样的界面:

interface Tuple<T, N extends number> extends ArrayLike<T> //or Array<T> {
   0: T
   length: N
}

但是这个实现有一个问题,编译器允许我访问无效索引处的元素,如下所示:

let tuple: Tuple<number, 2> = [1,2]
tuple[4] //No compiler error, although the index is out of bounds.

我能找到的另一个实现 (here) 是使用递归条件类型:

type Tuple<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;

这解决了索引问题,但我不能再将这种类型实现到我的 Vector 类中,因为它是一个类型联合。 我收到以下错误:

一个类只能实现一个对象类型或对象的交集 具有静态已知成员的类型.ts(2422)

有什么方法可以按照我想要的方式定义元组类型吗?

其他问题: 有没有办法在没有数组类的所有方法的情况下实现元组类型?我还不确定 Vector 是否具有过滤、排序和其他不相关方法等方法是否有意义。特别是可以改变元组大小的方法会非常糟糕。我知道在第一个实现中使用 ArrayLike 而不是 Array 至少可以实现这一点。省略也可以,但是手动指定每个方法名称真的很烦人。

【问题讨论】:

  • 对于“动态”键的唯一启示类是使用索引签名;但是正如您所看到的,这意味着所有(例如)number-valued 键都可以具有属性值,而不仅仅是您想要的值。一旦您尝试使用条件/通用/等键,您就不能直接在类中实现它。有一些变通方法,例如 this... 您是否有兴趣将这种变通方法作为答案?
  • @jcalz 感谢您的回答,这基本上是我正在寻找的解决方案(最终除了一些小的调整)。我不能 100% 确定我是否会以这种方式实现它,但这绝对是我的问题的一个很好的答案。

标签: typescript typescript-generics


【解决方案1】:

正如您所注意到的,编译器仅允许 interface(或将同名 interface 带入作用域的 class)具有“静态已知”键。所以你不能扩展或实现任何键是延迟泛型的类型:

class Foo<K extends string> implements Record<K, string> { } // error!
// A class can only implement an object type or intersection of 
// object types with statically known members.
interface Bar<K extends string> extends Record<K, string> { } // error!
// An interface can only extend an object type or intersection of 
// object types with statically known members.

对于您的Vector&lt;N&gt; 类型,您希望有从0 到比N 小一的数字(或类似数字)键。由于NVector&lt;N&gt; 的定义中是通用的,这意味着确切的键集不是静态已知的(例如,6 是键吗?在构造实例之前你不会知道),所以你可以不要将Vector&lt;N&gt; 设为classinterface

如果您允许每个 number-valued 键存在,您可以使用静态已知数字index signature。但是您特别不想允许N 或更大(或-1Math.PI 等)的键的属性。所以让我们忘记使用索引签名吧。


您可以在此处使用的解决方法是将Vector&lt;N&gt; 实例的类型描述为type 别名,而不是interfaceclass。您可以声明有一个名为 Vector 的值,其类型具有 construct signature 产生 Vector&lt;N&gt; 实例。

然后,您可以制作在运行时按您想要的方式工作的东西,但其类型并不完全符合您的需求。例如,您可以使用索引签名和 Vector&lt;N&gt; 所需的所有功能创建 class _Vector { ... }

最后,您可以将_Vector 分配给名为Vector 的变量,并且assert 前者具有后者的类型。它基本上回避了对静态已知键的要求。

让我们分阶段进行:


描述类型:

从您的 recursive conditional 定义开始,长度为 N 的元组:

type Tuple<T, N extends number> = N extends N ? number extends N ? 
  T[] : { length: N } & _TupleOf<T, N, []> : never;
type _TupleOf<T, N extends number, R extends unknown[]> = 
  R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;

我们将Vector&lt;N&gt; 描述为具有Tuple&lt;number, N&gt; 的所有类似数字键的类型,以及Nlength,以及您想要的任何Vector 特定的方法或属性类:

type Vector<N extends number> =
  Omit<Tuple<number, N>, keyof any[]> &
  {
    length: N;
    vectorMethod(): void;
  };

而构造函数可以这样描述:

interface VectorConstructor {
  new <N extends number>(...init: Tuple<number, N>): Vector<N>;
}

实现一个在运行时工作的类:

我们将只使用索引签名:

class _Vector {
  [k: number]: number | undefined;
  length: number;
  constructor(...init: number[]) {
    this.length = init.length;
    for (const [i, v] of init.entries()) {
      this[i] = v;
    }
  }
  vectorMethod() {
    console.log("something here");
  }
}

断言实现的类构造了所需的实例类型:

const Vector = _Vector as VectorConstructor;

让我们看看它是否有效:

const v = new Vector(0, 1, 4, 9, 16, 25); // Vector<6>
/* v: {
    length: 6;
    0: number;
    1: number;
    2: number;
    3: number;
    4: number;
    5: number;
    vectorMethod: () => void;
} */

v[3]++; // okay
v.vectorMethod();
v[10]; // error

看起来不错。值vVector&lt;6&gt; 类型,它具有我们想要的六个数字索引、6 类型的length 属性和我们添加的vectorMethod()。因此它的行为符合预期。


当然,这里有一些警告。显而易见的一点是编译器可能不会在您的实现中捕获错误,因为您使用的是类型断言。所以你需要小心。一个不太明显的问题是,您正在彻底解决静态已知键的问题。如果有人稍后出现并尝试扩展或实现您的新类类型,他们将得到与您最初遇到的相同的错误:

class Oops<N extends number> extends Vector<N> { } // error!
/* Base constructor return type 'Vector<N>' is not an object type
  or intersection of object types with statically known members. */

所以如果你想要一个类层次结构,你需要继续使用上面的技巧,这可能会变得乏味:

class _Sigh extends _Vector {
  anotherMethod() {

  }
}
type Sigh<N extends number> = Vector<N> & { anotherMethod(): void };
interface SighConstructor { new <N extends number>(...init: Tuple<number, N>): Sigh<N>; }
const Sigh = _Sigh as SighConstructor;

Playground link to code

【讨论】:

  • 再次感谢您非常详细的解释!很遗憾,这种解决方法显然是解决我的问题的唯一方法......但至少它按预期工作。
猜你喜欢
  • 2013-01-26
  • 1970-01-01
  • 2011-08-25
  • 1970-01-01
  • 1970-01-01
  • 2020-07-31
  • 2021-09-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多