Typesript argument depends on another argument with a default value

Viewed 174

I am trying to make a function in typescript where the second, optional argument, is a key of the first argument. Without the optional argument, the function I want looks like

function getVal<T>(obj: T, key: keyof T) {
    return obj[key];
}

However, I would like key to be optional and take the default value of "id". But, the function

function getValBad<T>(obj: T, key: keyof T = "id") {
    return obj[key];
}

doesn't typecheck, since typescript doesn't know if T has a key of id. A partial fix to this problem is to write

function getValOk<T extends { id: any }>(obj: T, key: keyof T = "id") {
    return obj[key];
}

however, this forces T to always have a key of id.

My question is, can I write a function getValGood so that getValGood({id: 1}) typechecks, getValGood({ID: 1}, "ID") typechecks and getValGood({ID: 1}) doesn't type check. If so, how do I represent getValGood in typescript?

2 Answers

First, you'll probably want to use another generic argument to type the returned value properly. (Otherwise, obj[key] will return a union of all possible values on the object, not just the type at key)

You can overload the getVal function to take either an object and a key that's a property of the object (2 generics), OR use only a single generic { id: V } and return something of type V:

type GetVal = {
    <T, K extends keyof T>(obj: T, key: K): T[K];
    <V>(obj: { id: V }): V;
};

const getVal: GetVal = (obj: Record<string, unknown>, key = 'id') => {
    return obj[key];
};

const result1 = getVal({ foo: 'foo' }, 'foo');
const result2 = getVal({ foo: 'foo' }, 'doesntexist'); // Fails

const result3 = getVal({ id: 'val' });
const result4 = getVal({ }); // Fails

Demo

I think you should use function overloads for this:

function getVal<V>(obj: {id: V}): V;
function getVal<T, K extends keyof T>(obj: T, key: K): T[K];

function getVal(obj: any, key = 'id') {
    return obj[key];
}

Note that when a function has overload signatures, Typescript does not check for you that the function implementation actually conforms to each overload signature individually; it's up to you to make sure that the implementation you write is type-safe according to the overload signatures.

Typescript will still at least make sure that each call to the function is correctly-typed.

Playground Link

Related