正如您所注意到的,编译器仅允许 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<N> 类型,您希望有从0 到比N 小一的数字(或类似数字)键。由于N 在Vector<N> 的定义中是通用的,这意味着确切的键集不是静态已知的(例如,6 是键吗?在构造实例之前你不会知道),所以你可以不要将Vector<N> 设为class 或interface。
如果您允许每个 number-valued 键存在,您可以使用静态已知数字index signature。但是您特别不想允许N 或更大(或-1 或Math.PI 等)的键的属性。所以让我们忘记使用索引签名吧。
您可以在此处使用的解决方法是将Vector<N> 实例的类型描述为type 别名,而不是interface 或class。您可以声明有一个名为 Vector 的值,其类型具有 construct signature 产生 Vector<N> 实例。
然后,您可以制作在运行时按您想要的方式工作的东西,但其类型并不完全符合您的需求。例如,您可以使用索引签名和 Vector<N> 所需的所有功能创建 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<N> 描述为具有Tuple<number, N> 的所有类似数字键的类型,以及N 的length,以及您想要的任何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
看起来不错。值v 是Vector<6> 类型,它具有我们想要的六个数字索引、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