0

Suppose I have a generic utility class that is supposed to interface with a class and has a method setPropertiesToTrue that takes in a list of class properties, and sets those properties inside the instance of the class to true.

class SomeObject{
  x: boolean 
  y: boolean
  z: string

  constructor(x: boolean,y: boolean,z: string){
    this.x = x,
    this.y = y,
    this.z = z
  } 
}

class ProperySetter<TSomeObject>{
  objectContext: TSomeObject

  constructor(objectContext: TSomeObject){
    this.objectContext = objectContext
  }

  setPropertiesToTrue(properties: Array<keyof TSomeObject>){
    properties.forEach(property => {
      this.objectContext[property] = true
    })
  }
}


const someObject = new SomeObject(false, false, "hello")

const propertySetter = new ProperySetter(someObject);

propertySetter.setPropertiesToTrue(["x", "y"])

console.log(someObject.x) // changed from false to true
console.log(someObject.y) // changed from false to true
console.log(someObject.z) // unchanged


Here is a TS Playground example of more or less what I'm trying to do.

In the example above, (at line 22 in the playground), it appears typescript has no the idea that keys being passed as properties correspond to boolean values inside the generic TSomeObject. So I’m getting the error:

Type 'boolean' is not assignable to type 'TSomeObject[keyof TSomeObject]'.

And this makes sense because keyof TSomeObject correspond to all keys of TSomeObject not just the keys corresponding to the boolean keys.

Is there a way to get around this?

1 Answer 1

1

You can strengthen the constrain of the input array to include only the keys that are boolean values. However, TS is not smart enough to see the know that the keys are valid in the array. Therefore, you're going to just have to go with a cast to get the code to work. Here is an example of the working code with the better constraints on the keys type:

class SomeObject{
  x: boolean 
  y: boolean
  z: string

  constructor(x: boolean, y: boolean, z: string){
    this.x = x,
    this.y = y,
    this.z = z
  } 
}

class ProperySetter<TSomeObject>{
  objectContext: TSomeObject

  constructor(objectContext: TSomeObject){
    this.objectContext = objectContext
  }

  setPropertiesToTrue(properties: Array<keyof { [K in keyof TSomeObject as TSomeObject[K] extends boolean ? K : never]: 0 }>){
    properties.forEach(property => {
      (this.objectContext as any)[property] = true
    })
  }
}


const someObj = new SomeObject(true, true, "hey");
const setter = new ProperySetter(someObj);
setter.setPropertiesToTrue(["x", "y"])

// @ts-expect-error not a boolean property
setter.setPropertiesToTrue(["z"]);

TypeScript Playground Link

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for your response. I always thought casting as any was an indication of a code smell or bad typing. Is this a valid use case for it? Also is there a reason you used 0 instead of never while typing properties?
@canecse yes, there are valid use cases for any such as if u want to match any instance of generic types. (E.g. Person<any>) which may not be possible with certain types. Furthermore, typescript is not full proof and doesn't do full inference on types so it loses information in the hopes of higher performance and other reasons. Also, there's not really a reason for using 0 over never as the values are never used in the mapped type.
I see. I learned something new today. Thanks a lot!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.