对typescript非常陌生,我正在尝试移植一些c#代码。我需要能够通过属性字符串名称设置实例属性。
在c#中,我用反射来做这件事,但是在typescript中我怎么做呢?
下面是一个简化的例子:
class BaseClazz {
baseA?: string;
baseB?: string;
}
class DerivedClazz extends BaseClazz {
derived1?: number;
derived2?: number;
array1?: number[];
array2?: number[];
}
我想要一个函数,可以设置各种属性的字符串名称:
function assign(clazz: BaseClass, propertyName: string, value: any) {
...
}
理论上我可以这样调用它,4个属性中的每一个都将被设置:
test:BaseClazz = new DerivedClazz();
assign(test, "baseA", "a");
assign(test, "baseB", "b");
assign(test, "derived1", 1);
assign(test, "derived2", 2);
assign(test, "array1", [1, 2, 3, 4]);
assign(test, "array2", [5, 6, 7, 8]);
由于JavaScript中的所有对象类型本质上都是属性包,因此您可以将assign()
编写为接受任何对象作为其第一个参数的泛型函数(而不仅仅是BaseClazz
的子类型):
function assign<T extends object, K extends keyof T>(
obj: T, key: K, val: T[K]
) {
obj[key] = val; // okay
}
这里obj
是约束于object
的泛型T
,key
是约束于T
的键(使用keyof
类型运算符)的泛型K
,val
是索引访问类型T[K]
,即obj[key]
的属性类型。
如果你真的想将函数限制为BaseClazz
的子类型,你可以写T extends BaseClazz
而不是T extends object
,但除非你需要assign()
中BaseClazz
的其他功能,否则没有太多理由。
让我们确保它工作:
const test = new DerivedClazz();
assign(test, "baseA", "a"); // okay
assign(test, "baseB", 123); // error!
// -----------------> ~~~
// Argument of type 'number' is not assignable to parameter of type 'string'
assign(test, "baseB", "b"); // okay
assign(test, "baseC", "x"); // error!
// Argument of type '"baseC"' is not assignable to parameter of type 'keyof DerivedClazz'
assign(test, "derived1", 1); // okay
assign(test, "derived2", 2); // okay
assign(test, "array1", [1, 2, 3, 4]); // okay
assign(test, "array2", [5, 6, 7, 8]); // okay
看起来不错。如果你传入的key
在test
上不存在,编译器会报错,如果你传入的value
不是对应于索引test
的属性类型(键为key
),编译器会报错。
这是TypeScript通常为泛型提供的类型安全。这不是完全合理的。例如,如果key
是联合类型,那么value
也被允许是相应的联合类型,这可能很奇怪:
assign(test, Math.random() < 0.999 ? "derived1" : "array1", [1]); // okay,
// but 99.9% chance of being bad
但是不健全贯穿整个语言,有意为之,不管是好是坏(参见microsoft/TypeScript#9825的注释)。还有其他方法可以将错误的东西放入属性中,因为TypeScript在其属性中将对象类型视为协变的,这可能会很奇怪:
interface Foo {
baseA?: string | number;
}
const foo: Foo = test; // okay because DerivedClazz extends Foo
assign(foo, "baseA", 123); // okay, but oops
foo.baseA = 456; // same thing directly
console.log(test.baseA.toUpperCase()); // 💥 RUNTIME ERROR!
因此,在将子类型与属性分配相结合时要小心!
Playground链接到代码