Search code examples
typescriptdefault-argumentskeyof

Typesript argument depends on another argument with a default value


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?


Solution

  • 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