Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

typescript

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?

like image 940
binki Avatar asked Jun 14 '18 06:06

binki


People also ask

What does Keyof Typeof do?

keyof is a keyword in TypeScript which is used to extract the key type from an object type.

What does Keyof return?

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.

What does Keyof mean in TypeScript?

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.


1 Answers

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
like image 151
artem Avatar answered Oct 04 '22 03:10

artem