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.
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
}
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