Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assignment involving generic property of generic object fails to typecheck correctly within generic function

I have a generic function that reads or writes the caller-chosen property of a given object. I'm using type constraints to ensure that the key passed is for a property that is assignable to or from the relevant type. Calling code appears to typecheck correctly. The usage of the object's property within the implementation does not typecheck as expected.

In this example I use boolean as the expected type. I've commented the lines that are not typechecking as expected. You can also see this example in the typescript playground here.

How can I express the signature of booleanAssignmentTest so that the typechecker understands obj[key] has type boolean? Can it be done in a fashion that keeps the boolean itself generic to allow multiple such similar functions, that work with other types, to be defined uniformly?

type KeysOfPropertiesWithType<T, U> = {
  // We check extends in both directions to ensure assignment could be in either direction.
  [K in keyof T]: T[K] extends U ? (U extends T[K] ? K : never) : never;
}[keyof T];

type PickPropertiesWithType<T, U> = Pick<T, KeysOfPropertiesWithType<T, U>>;

function booleanAssignmentTest<T extends PickPropertiesWithType<T, boolean>, K extends KeysOfPropertiesWithType<T, boolean>>(obj: T, key: K): void {
    let foo: boolean = obj[key]; // Fine!
    let foo2: string = obj[key]; // No error, but there should be!
    obj[key] = true; // Error: "Type 'true' is not assignable to type 'T[K]'."
}

let foo = { aBool: false, aNumber: 33, anotherBool: false };
booleanAssignmentTest(foo, "aBool"); // Fine!
booleanAssignmentTest(foo, "anotherBool"); // Fine!
booleanAssignmentTest(foo, "aNumber"); // Error: working as intended!

I'm using tsc Version 3.4.5 in case it's relevant.

Update:

I found the following answer on a similar issue: https://stackoverflow.com/a/52047487/740958

I tried to apply their approach which is simpler and works a little better, however the obj[key] = true; statement still has the same issue.

function booleanAssignmentTest2<T extends Record<K, boolean>, K extends keyof T>(obj: T, key: K): void {
    let foo: boolean = obj[key]; // Fine!
    let foo2: string = obj[key]; // Error: working as intended!
    obj[key] = true; // Error: "Type 'true' is not assignable to type 'T[K]'."
}

let foo = { aBool: false, aNumber: 33, anotherBool: false };

booleanAssignmentTest2(foo, "aBool"); // Fine!
booleanAssignmentTest2(foo, "anotherBool"); // Fine!
booleanAssignmentTest2(foo, "aNumber"); // Error: working as intended!

This ^^ example on TS Playground.

like image 835
EdF Avatar asked May 13 '19 02:05

EdF


1 Answers

The first option (using KeysOfPropertiesWithType) does not work because typescript can't reason about conditional types that still contains unresolved type parameters (such as T and K in this example)

The second option does not work because T extends Record<K, boolean> means T can for example be { a: false } which would mean the assignment obj[key] = true would not be valid. Generally, the fact that T[K] must extends a type does not mean that inside the generic function we can assign any value to it, the constraint just tells us what the minimum requirement is for the value, we don't yet know the full contract T[K] requires.

A solution that does work, at least for your sample code, is to not use T at all. It does not seem necessary in this context:

function booleanAssignmentTest2<K extends PropertyKey>(obj: Record<K, boolean>, key: K): void {
    let foo: boolean = obj[key]; // Fine!
    let foo2: string = obj[key]; // Error: working as intended!
    obj[key] = true; // Ok now we know T[K] is boolean
}

let foo = { aBool: false, aNumber: 33, anotherBool: false };

booleanAssignmentTest2(foo, "aBool"); // Fine!
booleanAssignmentTest2(foo, "anotherBool"); // Fine!
booleanAssignmentTest2(foo, "aNumber"); // Error: working as intended!

If your example is more complicated, please provide a full example, although generally the solution will be use a type assertion if you are sure the value is assignable to T[K], so this is a possible solution:

function booleanAssignmentTest2<T extends Record<K, boolean>, K extends keyof T>(obj: T, key: K): void {
    let foo: boolean = obj[key]; // Fine!
    let foo2: string = obj[key]; // Error: working as intended!
    obj[key] = true as T[K]; // ok now
}
like image 98
Titian Cernicova-Dragomir Avatar answered Oct 12 '22 20:10

Titian Cernicova-Dragomir