Home > Net >  Setting object property by string name
Setting object property by string name

Time:11-22

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()); //            
  • Related