E. Guzman
E. Guzman

Reputation: 11

How to map types of two arguments of function in Typescript?

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

Answers (2)

Acid Coder
Acid Coder

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

enter image description here playground

IsUnion

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

T.J. Crowder
T.J. Crowder

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

Related Questions