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
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
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
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