Very new to typescript and am trying to port some C# code. I need to be able to set an instances properties by property string name.
In C# I do this with reflection, but how can I do this in typescript?
The following is a simplified example:
class BaseClazz {
baseA?: string;
baseB?: string;
}
class DerivedClazz extends BaseClazz {
derived1?: number;
derived2?: number;
array1?: number[];
array2?: number[];
}
I want a function that can set the various properties by their string name:
function assign(clazz: BaseClass, propertyName: string, value: any) {
...
}
In theory I can call it as follows and each of the 4 properties would be set:
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]);
CodePudding user response:
Since all object types in JavaScript are essentially property bags, you can write assign()
as a generic function that accepts any object for its first argument (not just a subtype of BaseClazz
):
function assign<T extends object, K extends keyof T>(
obj: T, key: K, val: T[K]
) {
obj[key] = val; // okay
}
Here obj
is of generic type T
constrained to object
, and key
is of generic type K
constrained to the keys of T
(using the keyof
type operator), and val
is of the indexed access type T[K]
, which is the type of the property at obj[key]
.
If you really want to limit the function to be for subtypes of BaseClazz
you can write T extends BaseClazz
instead of T extends object
, but unless you need other functionality of BaseClazz
inside assign()
there's not much reason to.
Let's make sure this works:
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
Looks good. The compiler complains if you pass in a key
that is not known to exist on test
, and it complains if you pass in a value
that is not of the type of the property corresponding to indexing into test
with the key at key
.
This is about as type-safe as TypeScript typically gets for generics. It's not perfectly sound. For example, if the key
is of a union type then the value
is also allowed to be of the corresponding union type, which could be weird:
assign(test, Math.random() < 0.999 ? "derived1" : "array1", [1]); // okay,
// but 99.9% chance of being bad
But unsoundness runs through the language, intentionally, for better or worse (see this comment on microsoft/TypeScript#9825). There are other ways to put the wrong thing into a property, since TypeScript treats object types as covariant in their properties, which again, could be weird:
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()); //