typescript 通过字符串名称设置对象属性

nqwrtyyt  于 2022-11-26  发布在  TypeScript
关注(0)|答案(1)|浏览(172)

非常新的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]);
nue99wik

nue99wik1#

由于JavaScript中的所有对象类型本质上都是属性包,因此可以将assign()编写为generic函数,该函数的第一个参数接受任何对象(而不仅仅是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之索引键的泛型型别K(使用the keyof type operator),而valindexed access typeT[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代码链接

相关问题