当前返回类型 (string[]) 是有意的。为什么?
考虑这样的一些类型:
interface Point {
x: number;
y: number;
}
你写一些这样的代码:
function fn(k: keyof Point) {
if (k === "x") {
console.log("X axis");
} else if (k === "y") {
console.log("Y axis");
} else {
throw new Error("This is impossible");
}
}
让我们问一个问题:
在一个类型良好的程序中,对fn 的合法调用能否遇到错误情况?
想要的答案当然是“不”。但是这和Object.keys有什么关系呢?
现在考虑这个其他代码:
interface NamedPoint extends Point {
name: string;
}
const origin: NamedPoint = { name: "origin", x: 0, y: 0 };
请注意,根据 TypeScript 的类型系统,所有NamedPoints 都是有效的Points。
现在让我们再写一点代码:
function doSomething(pt: Point) {
for (const k of Object.keys(pt)) {
// A valid call iff Object.keys(pt) returns (keyof Point)[]
fn(k);
}
}
// Throws an exception
doSomething(origin);
我们的良好类型程序刚刚抛出异常!
这里出了点问题!
通过从Object.keys 返回keyof T,我们违反了keyof T 构成一个详尽列表的假设,因为对对象的引用并不意味着引用的类型 '不是值的类型的超类型。
基本上,(至少)以下四件事之一不可能是真的:
-
keyof T 是 T 的键的详尽列表
- 具有附加属性的类型始终是其基本类型的子类型
- 通过超类型引用为子类型值起别名是合法的
-
Object.keys 返回keyof T
丢弃第 1 点会使keyof 几乎毫无用处,因为这意味着keyof Point 可能不是"x" 或"y" 的某个值。
丢弃第 2 点完全破坏了 TypeScript 的类型系统。不是一个选项。
丢弃第 3 点也完全破坏了 TypeScript 的类型系统。
扔掉第 4 点很好,让你,程序员,想想你正在处理的对象是否可能是你认为你拥有的东西的子类型的别名。
使这个合法但不矛盾的“缺失功能”是Exact Types,这将允许您声明一个不受约束的新类型种类到第 2 点。如果存在此功能,则大概可以使Object.keys 仅针对声明为精确的Ts 返回keyof T。
附录:当然是泛型?
评论者暗示 Object.keys 可以安全地返回 keyof T 如果参数是一个通用值。这仍然是错误的。考虑:
class Holder<T> {
value: T;
constructor(arg: T) {
this.value = arg;
}
getKeys(): (keyof T)[] {
// Proposed: This should be OK
return Object.keys(this.value);
}
}
const MyPoint = { name: "origin", x: 0, y: 0 };
const h = new Holder<{ x: number, y: number }>(MyPoint);
// Value 'name' inhabits variable of type 'x' | 'y'
const v: "x" | "y" = (h.getKeys())[0];
或者这个例子,它甚至不需要任何显式类型参数:
function getKey<T>(x: T, y: T): keyof T {
// Proposed: This should be OK
return Object.keys(x)[0];
}
const obj1 = { name: "", x: 0, y: 0 };
const obj2 = { x: 0, y: 0 };
// Value "name" inhabits variable with type "x" | "y"
const s: "x" | "y" = getKey(obj1, obj2);