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?
keyof is a keyword in TypeScript which is used to extract the key type from an object type.
keyof T returns a union of string literal types. The extends keyword is used to apply constraints to K, so that K is one of the string literal types only. extends means “is assignable” instead of “inherits”' K extends keyof T means that any value of type K can be assigned to the string literal union types.
The keyof type operatorThe keyof operator takes an object type and produces a string or numeric literal union of its keys. The following type P is the same type as “x” | “y”: type Point = { x : number; y : number }; type P = keyof Point ; type P = keyof Point.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With