我有一个遗留项目,它有一个基类,它表示在运行时构建的复杂对象。这些对象具有状态,并具有状态属性的 getter/setter 方法。
在某些情况下,状态属性是众所周知的,在这种情况下我想强制执行严格的类型检查。但是,当使用未知的状态属性时,我需要后备。
以下是实践中的基本概念:
interface KnownProps {
name: string;
age: number;
}
class Foo {
private state:Record<string, unknown> = {};
set<P extends keyof KnownProps>(key: P, value:KnownProps[P]): void;
set(key:string, value:unknown):void;
set(key:string, value:unknown):void {
this.state[key] = value;
}
get<P extends keyof KnownProps>(key:P):KnownProps[P];
get(key:string):unknown;
get(key:string):unknown {
return this.state[key];
}
}
const foo = new Foo();
// Overloads match as desired:
foo.set("name", "Steve"); // key: "name", value:string
foo.set("age", 37); // key: "age", value: number
foo.set("birthday", new Date()); // key: "birthday", value: unknown
const name = foo.get("name"); // string
const age = foo.get("age"); // number
const birthday = foo.get("birthday"); // unknown;
问题在于 TypeScript 在尝试匹配重载方面非常慷慨(因为它应该如此),因此如果我传入具有无效类型的已知属性,我不会收到任何错误报告:
// Should throw errors
foo.set("name", 37); // expected a string
foo.set("age", "Steve"); // expected a number
我认为使用条件类型可能会“捕获”已知属性并引发错误:
{
set<P extends keyof KnownProps, V>(key: P, value:V extends KnownProps[P] ? KnownProps[P] : never): void;
}
但是,唉,TypeScript 仍然通过“后备”重载慷慨地接受不正确的类型定义。
在“键”上使用条件类型,而不是值。从 TypeScript 5.0 开始,请确保在类型参数上使用
const
修饰符,以便它与文字属性名称匹配,而不是将类型扩展为 string
:
{
set<const P>(key: P, value:P extends keyof KnownProps ? KnownProps[P] : unknown): void;
set(key:string, value:unknown):void {
this.state[key] = value;
}
}
您现在应该会收到预期的错误:
// Overloads match as desired:
foo.set("name", "Steve"); // key: "name", value:string
foo.set("age", 37); // key: "age", value: number
foo.set("birthday", new Date()); // key: "birthday", value: unknown
const name = foo.get("name"); // string
const age = foo.get("age"); // number
const birthday = foo.get("birthday"); // unknown;
// Known properties now apply strict type checking:
foo.set("name", 37);
// ~~
// Argument of type 'number' is not assignable to parameter of type 'string'.
foo.set("age", "Steve");
// ~~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'number'.
在 TypeScript 5.0 之前,您可以遵循上面的建议,但是您必须通过调用
as const
来使用 set()
才能获得相同的行为:
/////////////////////////
// BEFORE TYPESCRIPT 5.0:
{
set<P>(key: P, value:P extends keyof KnownProps ? KnownProps[P] : unknown): void;
set(key:string, value:unknown):void {
this.state[key] = value;
}
}
foo.set("name" as const, 37);
// ~~
// Argument of type 'number' is not assignable to parameter of type 'string'.
foo.set("age" as const, "Steve");
// ~~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'number'.