Please let me know if this is a duplicate, but I can't find an answer to this. Maybe because I'm missing the right words to search for as you can see in the title :/
Is it possible to define a generic function in TypeScript that receives two arguments (a key and a value) and verifies that the type of the given value matches the type defined in a Record type depending on the given key? This may sound confusing. Let me give you a simple example:
import { setAttribute } from './attribute_helper'
type Human = {
name: (name: string) => void
age: (age: number) => void
}
function setName(name: string): void {
// ...
}
setAttribute<Human>('name', setName) // valid
setAttribute<Human>('age', setName) // invalid
// should work with a different Record type:
setAttribute<Cat>('food', giveFood)
Here's how this would work if one specific Record type can be used in the definition of the function:
type Human = {
name: (name: string) => void
}
function setAttribute<T extends keyof Human>(key: T, value: Human[T]): void {
}
function setName(name: string): void {
// ...
}
setAttribute('name', setName)
But in my case, setAttribute
is imported from a different file and needs to work with different Record types it doesn't know about at definition time. It needs to be typed when it is called.
Here's a sketch which unfortunately is not valid TypeScript, but it may help to illustrate the problem:
// attribute_helper.ts
export function setAttribute<T>(key: keyof T as V, value: V): void {
}
CodePudding user response:
Conceptually, what you want is a function like
function setAttribute<T, K extends keyof T>(key: K, value: T[K]): void { }
which is generic both in the type of the record ou'd call it like
setAttribute<MyRecord>('name', setName); // want it to be okay, but error!
Unfortunately, this is not valid TypeScript. You can't manually specify some generic parameters (like putting MyRecord
in for T
in this case) and have the compiler infer the rest of them (like inferring "name"
for K
in this case). That would require partial type parameter inference as requested in microsoft/TypeScript#26242. Right now it's not possible.
It might be tempting to try to use generic parameter defaults to get this behavior, like
function setAttribute<T, K extends keyof T = keyof T>(key: K, value: T[K]): void { }
but while this does let setAttribute<MyRecord>('name', setName)
compile, it does not work the way you want; the K
type parameter will not be inferred from key
; instead it will simply use the default, which is keyof T
in this case, which is not what you want. You want K
to be "name"
so that the compiler could accept setName
but reject setAge
for the second parameter.
So right now this is not possible. A single generic function requires callers to either manually specify all its type parameters, or let the compiler infer all its type parameters.
The workarounds I know of are either to pass in a dummy parameter of type T
so that the compiler can infer both T
and K
:
function setAttributeDummy<T, K extends keyof T>(dummyT: T, key: K, value: T[K]): void { }
const dummyRecord: MyRecord = null!;
setAttributeDummy(dummyRecord, 'name', setName) // valid
setAttributeDummy(dummyRecord, 'age', setName) // invalid
Or to make a curried function that lets you manually specify T
and returns another function that infers K
:
function setAttributeCurry<T>(): <K extends keyof T>(k: K, v: T[K]) => void {
return (k, v) => {
/* your impl here */
}
}
setAttributeCurry<MyRecord>()('name', setName); // valid
setAttributeCurry<MyRecord>()('age', setName); // invalid
This works, but is awkward looking and repetitive. If you're going to use a curried function you might as well use it just once for any particular value of T
, like this:
const setMyRecordAttribute = setAttributeCurry<MyRecord>();
setMyRecordAttribute('name', setName); // valid
setMyRecordAttribute('age', setName); // invalid
And then again for any other value like
const setSomeOtherAttribute = setAttributeCurry<{ a: string, b: number }>();
setSomeOtherAttribute('b', 123); // okay
setSomeOtherAttribute('a', 123); // error