Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Write a function that uses lookup types on generics

I want to write a function that does three things:

  • operates on a generic type T
  • accepts a key: K of T where T[K] must be a boolean
  • assigns a value to T[K]

I'm following this guide which hints at this possibility:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

However, it seems that I can only accomplish my goal if my function operates on concrete types. To compute the boolean property names of T, I've modified the first line from the example like this:

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

It works if I use a concrete type:

class Thing {
  isGreat: boolean;
}

function assignToThing(thing: Thing, key: BoolPropNames<Thing>) {
  thing[key] = false;
}

But if I try to operate on a generic, it doesn't:

function assign<T>(thing: T, key: BoolPropNames<T>) {
  thing[key] = false;
}

When I try to do this, TypeScript gives me the following error:

(parameter) key: { [K in keyof T]: T[K] extends boolean ? K : never; }[keyof T]
Type 'false' is not assignable to type 'T[{ [K in keyof T]: T[K] extends boolean ? K : never; }[keyof T]]'.ts(2322)

Why doesn't TypeScript allow me to operate on generics this way, and what can I do to fix it?

like image 645
Samo Avatar asked Sep 09 '26 18:09

Samo


1 Answers

TS is not able to compute the result of BoolPropNames<T> as it is computed when T is given, differently then in previous examples where you had no type variable, but some specific type. As for example BoolPropNames<Thing> is computed into isGreat and thing[key] is known as to be boolean.

BoolPropNames<T> cannot be computed before so it does not know that result will be a key which gives us boolean. And if it doesn't know that think[key] is boolean then you cannot assign to it boolean.

In order to fix this, we can for example apply argument which will be considered as T[K] type. Then TS doesn't need to compute the type, as it will always be value of type at the given key. Consider following code:

function assign<T, K extends BoolPropNames<T>>(thing: T, key: K, value: T[K]) {
  thing[key] = value;
}
assign({ a: true }, 'a', false);

If third argument does not satisfy you, and you just want to set any value without putting it as argument we can also do some partial application in order to create such behavior. Consider:

// function which creates assign function
const createAssign = <T, K extends BoolPropNames<T> = BoolPropNames<T>>
(value: T[K]) => (thing: T, key: K) => {
  thing[key] = value;
}
// below assign function is created for type `Example`
type Example = { a: boolean };
const assign = createAssign<Example>(false); 
assign({a: true}, 'a');

By partial application we have created assign function which works in the same way your original one was working, so it is assigning false into only boolean fields. By this way you can make all such functions.

Of course the downside is that we need to generate such for every type by setting generic type variable, so it is not so polymorphic as solution with different order of arguments.

like image 194
Maciej Sikora Avatar answered Sep 11 '26 20:09

Maciej Sikora



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!