Home > other >  Generic function ensuring the type of a given value matches the type defined a Record for the given
Generic function ensuring the type of a given value matches the type defined a Record for the given

Time:11-03

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

Playground link to code

  • Related