TheGr8_Nik
TheGr8_Nik

Reputation: 3200

Keyof as generic

I'm trying to force someone that want to update an object to pass a the key to update with a correct value type. As seen in the playground typescript seems unable to understand that if the key has a certain value the value variable has a correct type. While calling the function the type checking is correct and don't allow to pass inconsistent values with the key.

type Person = {
    name: string;
    age: number;
};

function update<T extends keyof Person>(orig: Person, key: T, value: Person[T]): Person {
    if (key === "name") {
        return {
            ...orig,
            name: value  // Error: number not assignable to string
        }
    } else if (key === "age") {
        return {
            ...orig,
            age: value  // Error: string not assignable to number
        }
    } else {
        return orig
    }
}

let unknownPerson = {
    name: "Unknown",
    age: 999
};
unknownPerson = update(unknownPerson, "age", 10);
unknownPerson = update(unknownPerson, "age", "15");  // Ok expected number

Upvotes: 3

Views: 1058

Answers (2)

jcalz
jcalz

Reputation: 330316

The compiler is technically correct to warn you inside the implementation of update(). The generic type parameter T can be "name" or "age", but it can also be the union type "name" | "age".
It is possible (though not likely) for someone to call update() with a value that doesn't match with key, because of this. For example:

update(unknownPerson, Math.random() < 2 ? "name" : "age", 100); // no error
// function update<"name" | "age">(
//   orig: Person, key: "name" | "age", value: string | number): Person

So the compiler does not assume that checking key has any implication on the type of value. If key === "name", the compiler can narrow the type of key, but it does not narrow the type parameter T. And hence the error.

This is a known issue in TypeScript, and there are a few open feature requests intended to deal with it. See microsoft/TypeScript#33014 and microsoft/TypeScript#27808 to read more. For now though, this is just how it is.

If you need to write generic functions this way, then you might have to use type assertions to suppress the error.


But in your case, you don't really need generics here. Instead, you can refactor the function signature so that the key and value function parameters are represented as elements of a rest tuple whose type is a discriminated union. Like this:

type KeyValueTuple<T> =
    keyof T extends infer K ? K extends keyof T ? [key: K, value: T[K]] : never : never;

function update(orig: Person, ...[key, value]: KeyValueTuple<Person>): Person {
    if (key === "name") {
        return {
            ...orig,
            name: value
        }
    } else if (key === "age") {
        return {
            ...orig,
            age: value
        }
    } else {
        return orig
    }
}

The type function KeyValueTuple<T> takes a type T and produces a union of tuples whose first element is the key type (generally a string literal type) and whose second element is the corresponding value type. You can see how this behaves with Person:

type PersonKeyValueTuple = KeyValueTuple<Person>;
// type PersonKeyValueTuple = [key: "name", value: string] | [key: "age", value: number]

And then after the orig argument, we package key and value into a rest argument of type ...[key, value]: KeyValueTuple<Person>. Note that inside the function implementation key and value are seemingly distinct variables, but the compiler treats them as destructured discriminated unions so checking key does have the required implication for the type of value.

And so the above compiles with no error.

Playground link to code

Upvotes: 3

seabeast
seabeast

Reputation: 333

You can first create a clone of the original object and then update the specified key with the given value:

function update<T extends keyof Person>(orig: Person, key: T, value: Person[T]): Person {
    const result = { ...orig };

    result[key] = value;

    return result;
}

This way you also don't have to implement special handling for each possible key.

Upvotes: 2

Related Questions