3

I am having trouble in Typescript passing default values to a function similar to pick from lodash.

The function accepts an object of known (non-generic) interface and a set of keys to pick and return from the object.

Regular (no default params) declaration of the function works properly, however, I do not seem able to set an array as a default value for the parameter that selects the properties to pick.

interface Person {
    name: string;
    age: number;
    address: string;
    phone: string;
}

const defaultProps = ['name', 'age'] as const;


function pick<T extends keyof Person>(obj: Person, props: ReadonlyArray<T> = defaultProps): Pick<Person, T> {    
    return props.reduce((res, prop) => {
        res[prop] = obj[prop];
        return res;
    }, {} as Pick<Person,T>);
}

const testPerson: Person = {
    name: 'mitsos',
    age: 33,
    address: 'GRC',
    phone: '000'
};

If you remove the default value = defaultProps it compiles successfully and the returned type is also correct from an example call such as: const testPick = pick(testPerson, ['name']);

However, setting the default value produces the following error:

Type 'readonly ["name", "age"]' is not assignable to type 'readonly T[]'.
  Type '"name" | "age"' is not assignable to type 'T'.
    '"name" | "age"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'keyof Person'.
      Type '"name"' is not assignable to type 'T'.
        '"name"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'keyof Person'.

How can I successfully pass the default values to the props param?

Typescript Playground link here

UPDATE

After playing around a bit I tried using conditional types and managed to get the function signature working, but having problems with reduce now not being recognized correctly:

interface Person {
    name: string;
    age: number;
    address: string;
    phone: string;
}

const defaultProps = ['name', 'age'] as const;

type DefaultProps = typeof defaultProps;

type PropsOrDefault<T extends keyof Person> = DefaultProps | ReadonlyArray<T>;

type PickedPropOrDefault<T extends PropsOrDefault<keyof Person>> = T extends DefaultProps ? Pick<Person, DefaultProps[number]> : Pick<Person, T[number]>;


function pick<T extends keyof Person>(obj: Person, props: PropsOrDefault<T> = defaultProps): PickedPropOrDefault<PropsOrDefault<T>> {    
    return props.reduce<PickedPropOrDefault<PropsOrDefault<T>>>((res, prop) => {
        res[prop] = obj[prop];
        return res;
    }, {} as PickedPropOrDefault<PropsOrDefault<T>>);
}

const testPerson: Person = {
    name: 'mitsos',
    age: 33,
    address: 'GRC',
    phone: '000'
};

const result = pick(testPerson) //  Pick<Person, "name" | "age">
const result2 = pick(testPerson, ['phone']) // Pick<Person, "phone">
const result3 = pick(testPerson, ['abc']) // expected error

Updated Playground

mitsos1os
  • 2,170
  • 1
  • 20
  • 34

2 Answers2

3

You can overload pick function:

interface Person {
    name: string;
    age: number;
    address: string;
    phone: string;
}

const defaultProps = ['name', 'age'] as const;

type DefaultProps = typeof defaultProps;

function pick(obj: Person): Pick<Person, DefaultProps[number]>
function pick<Prop extends keyof Person, Props extends ReadonlyArray<Prop>>(obj: Person, props: Props): Pick<Person, Props[number]>
function pick<T extends keyof Person>(obj: Person, props = defaultProps) {
    return props.reduce((res, prop) => ({
        ...res,
        [prop]: obj[prop]
    }), {} as Pick<Person, T>);
}

const testPerson = {
    name: 'mitsos',
    age: 33,
    address: 'GRC',
    phone: '000'
};

const result = pick(testPerson) //  Pick<Person, "name" | "age">
const result2 = pick(testPerson, ['phone']) // Pick<Person, "phone">
const result3 = pick(testPerson, ['abc']) // expected error

Playground

You can find more advanced pick typings in my article and other answers: First , second, third

UPDATE

There is a problem with props argument in this code:

function pick<T extends keyof Person>(obj: Person, props: PropsOrDefault<T> = defaultProps): PickedPropOrDefault<PropsOrDefault<T>> {
    return props.reduce((res, prop) => {
        return {
            ...res,
            [prop]: obj[prop]
        }
    }, {});
}

PropsOrDefault might be equal to this type:type UnsafeReduceUnion = DefaultProps | ReadonlyArray<'phone' | 'address'> You probably have noticed, that these arrays in the union are completely different. They have nothing in common.

If you will to call reduce:

declare var unsafe:UnsafeReduceUnion;

unsafe.reduce()

you will get an error, because reduce is not callable

  • 1
    It seems to work properly and makes sense...! I was mostly concentrated on achieving this with a single function type definition and completely skipped the function overloading part. Thanks! – mitsos1os Oct 25 '21 at 18:22
  • It happens to me all the time. You are welcome! – captain-yossarian from Ukraine Oct 25 '21 at 18:23
  • Hey, after getting triggered by your answer, I took a better look at conditional types and tried this approach (just for educational reasons, since it is getting quite complicated). I managed to make the function signature work properly, but `reduce` seems to be causing problems... Can you take a look at the updated playground in the original question update? – mitsos1os Oct 26 '21 at 09:46
0

Something like that? Updated TS Playground.

const defaultProps: ReadonlyArray<keyof Person> = ['name', 'age'] as const;

function pick<T extends keyof Person>(obj: Person, props: ReadonlyArray<T> = (defaultProps as ReadonlyArray<T>)): Pick<Person, T> {   

UPDATED:

TS Playground.

Ruslan Lekhman
  • 696
  • 5
  • 9
  • I do not think this is the right answer because this way `defaultProps` are typed as all the keys of Person. If you make a test call `const test1 = pick(testPerson, ['name']);` `test1` is properly typed as `Pick`. However, if you make the call `const test2 = pick(testPerson)` then `test2` is typed `Pick` while it should be typed as `Pick` UpdatedTs: https://shorturl.at/dhiqI – mitsos1os Oct 22 '21 at 18:09
  • Then we could set default types as well. Updated TS Playground in the answer. – Ruslan Lekhman Oct 22 '21 at 19:07