How do I require a keyof to be for a property of a specific type?

0

Issue

I am attempting to write a generic function that can toggle a boolean property in any object by key name. I read the release notes for TypeScript-2.8 and thought that conditional types are supposed to solve this type of issue. However, I cannot figure out how to write my function.

My function accepts the object to be modified and the name of the key to modify. To ensure that only keys for boolean properties are passed in, I used the conditional type expression T[K] extends boolean ? K : never. As I understand it, this should cause an error if I try to pass a key for a non-boolean property because T[K] would not satisfy extends boolean. But if I tried to pass a key for a boolean, then it should accept that K.

However, it seems that even with this conditional, TypeScript does not realize within the function that T[K] extends boolean must be true. So I can’t assign the value I read from the object back to the object. This results in the first error shown below. The second error is that type inference doesn’t seem to work for my function. In the calls below, only the second one passes TypeScript’s checks so far.

function invertProperty<T, K extends keyof T> (o:T, propertyName:(T[K] extends boolean ? K : never)) {
  o[propertyName] = !o[propertyName]; // Type 'false' is not assignable to type 'T[T[K] extends boolean ? K : never]'. [2322]
}

const myObject:IObject = {
  a: 1,
  b: true,
  c: 'hi',
};

invertProperty(myObject, 'b'); // Argument of type '"b"' is not assignable to parameter of type 'never'. [2345]
invertProperty<IObject, 'b'>(myObject, 'b'); // Works, but requires me to type too much.
invertProperty(myObject, 'a'); // Argument of type '"a"' is not assignable to parameter of type 'never'. [2345]
invertProperty<IObject, 'a'>(myObject, 'a'); // Argument of type '"a"' is not assignable to parameter of type 'never'. [2345]

interface IObject {
  a:number,
  b:boolean,
  c:string,
}

I think that if in my type constraint of K extends keyof T I could somehow also state and T[K] extends boolean it would do the right thing. It seems to me that it is an issue that I’m trying to use never in the argument’s type instead of being able to constrain the type parameter. But I can’t find any way to express that.

Any ideas of how to accomplish this with full type safety?

Solution

First, you can extract all keys of boolean properties using this construct (which converts keys of non-boolean values to never and takes a union of all keys/never, using fact that T | never is T):

type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];

Then, to make TypeScript happy about assigning boolean values to properties, you introduce intermediate type which is declared to have only boolean properties (unfortunately TypeScript cannot figure out this part on its own)

type OnlyBoolean<T> = { [k in BooleanKeys<T>]: boolean };

and you declare that generic type parameter of invertProperty is compatible with OnlyBoolean (which it is, it may contain extra non-boolean properties but it’s OK)

NOTE you might need different versions of the code depending of the version of the compiler, original code in this answer has stopped working with TypeScript 3.2:

// for TypeScript 3.1 or earlier
function invertProperty<T extends OnlyBoolean<T>>(o: T, propertyName: BooleanKeys<T>) {
    o[propertyName] = !o[propertyName];
}

// for TypeScript 3.2 or later
function invertProperty<T>(o: OnlyBoolean<T>, propertyName: keyof OnlyBoolean<T>) {
    o[propertyName] = !o[propertyName];
}


interface IObject {
    a: number;
    b: boolean;
    c: string;
}
const myObject:IObject = {
  a: 1,
  b: true,
  c: 'hi',
};



invertProperty(myObject, 'b'); // ok
invertProperty(myObject, 'a'); // error

Answered By – artem

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave A Reply

Your email address will not be published.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More