Reputation: 11
How to implement such functionality?
type SomeType = {
name: string;
quantity: number;
};
const someFunc = (
key: keyof SomeType /* what should be here? */,
value: SomeType[keyof SomeType] /* what should be here? */
) => {
// ...
};
someFunc("name", "John") // OK
someFunc("name", 10) // must be error
someFunc("quantity", "John") // must be error
someFunc("quantity", 10) // OK
I've tried this:
...
const someFunc = (
key: keyof SomeType
value: SomeType[keyof SomeType]
)
...
But that doesn't work and I understand why. So I don't know how to implement this.
Upvotes: 1
Views: 477
Reputation: 2756
not so ideal foolproof improvement over @T.J Crowder solution
It stops you from using union but does not check the correctness of value type
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true
type SomeType = {
name: string;
quantity: number;
age: number
};
const someFunc = <Key extends keyof SomeType>(
key: IsUnion<Key> extends true ? "No Union!" : Key,
value: SomeType[Key],
) => {
// ...
};
someFunc<"name" | "quantity">("name", 10) // error at argument 1, ideally should error at argument 2, but still an error regardless, a true negative case
someFunc<"quantity" | "age">("quantity", 10) // should not error, because both quantity and age are number, a false negative case
// still works as normal
someFunc("name", "John") // OK
someFunc("name", 10) // Error as desired
someFunc("quantity", "John") // Error as desired
someFunc("quantity", 10) // OK
as shown in the code, not only it error on the wrong argument, there is also a false negative case
but does it matter? No it doesn't
because you are forced to fix errors by removing the union regardless, clearing the false negative case will not open up to another type error
so the end result is, it will still leads you to the correct type
It is not an ideal method, but it works, it is an example of better be safe than wrong
One problem is this is not newbies friendly, it requires the study of the background problem to understand why the code is written in such way and why the error appear on the wrong argument or appear for no reason.
For those newbies, they will end up in unfruitful troubleshooting every single time, and we all know that the next thing they do is to doubt the purpose of their very own existence
There is no way you can explain this like you explaining to a 5 years old, so TS should take the blame
Upvotes: 2
Reputation: 1075785
I don't think there's a 100% foolproof way to do this. You can get close with a generic type parameter:
type SomeType = {
name: string;
quantity: number;
};
const someFunc = <Key extends keyof SomeType>(
key: Key,
value: SomeType[Key],
) => {
// ...
};
With that, your examples work:
someFunc("name", "John") // OK
someFunc("name", 10) // Error as desired
someFunc("quantity", "John") // Error as desired
someFunc("quantity", 10) // OK
But, because of unions, it isn't 100%. We can specify an explicit type argument for the type parameter, and then force it to take the wrong combination of key
and value
:
someFunc<"name" | "quantity">("name", 10) // wrong, but no error
That has ramifications for the code inside the function, since you'd expect narrowing the type of key
to narrow the type of value
, but it doesn't; see this question's answers.
Upvotes: 3