In TypeScript, I have arrays of objects like this
const props1 = [
{ propName: 'id', propValue: 1 },
{ propName: 'name', propValue: 'John' },
{ propName: 'age', propValue: 30 }
];
const props2 = [
{ propName: 'id', propValue: 2 },
{ propName: 'role', propValue: 'admin' }
];
const props3 = [
{ propName: 'job', propValue: 'developer' },
{ propName: 'salary', propValue: 1000 }
];
I want to create a function createObject
that I can pass an array of them to it, and it should return an object that its keys are propName
with values propValue
This is the desirable results type
const obj1 = createObject(props1); // Returned type should be: { id: number; name: string; age: number }
const obj2 = createObject(props2); // Returned type should be: { id: number; role: string }
const obj3 = createObject(props3); // Returned type should be: { job: string; salary: number }
This is my try, but doesn't return the correct type
function createObject<T extends { propName: string; propValue: any }[]>(propertiesObject: T) {
const obj: { [key: string]: any } = {};
for (const { propName, propValue } of propertiesObject) {
obj[propName] = propValue;
}
return obj;
}
CodePudding user response:
The first thing we need to do is change the arrays. Currently, each propName
is only of type string
and there is no way to extract the original literal type.
We need to initialize them with as const
.
const props1 = [
{ propName: 'id', propValue: 1 },
{ propName: 'name', propValue: 'John' },
{ propName: 'age', propValue: 30 }
] as const
const props2 = [
{ propName: 'id', propValue: 2 },
{ propName: 'role', propValue: 'admin' }
] as const
const props3 = [
{ propName: 'job', propValue: 'developer' },
{ propName: 'salary', propValue: 1000 }
] as const
Now the actual function.
type ToPrimitive<T> =
T extends string ? string
: T extends number ? number
: T extends boolean ? boolean
: T;
function createObject<
T extends readonly { propName: string; propValue: any }[]
>(propertiesObject: T): {
[K in T[number] as K["propName"]]: ToPrimitive<K["propValue"]>
} {
const obj: { [key: string]: any } = {};
for (const { propName, propValue } of propertiesObject) {
obj[propName] = propValue;
}
return obj as { [K in T[number] as K["propName"]]: ToPrimitive<K["propValue"]> };
}
We also need to add readonly
to the constraint of T
to allow our as const
arrays. The return type is a simple mapped type which maps over the elements K
of T[number]
and uses K["propName"]
as the keys.
K["propValue"]
currently also holds the narrowed literal types. But we actually want to widen them here back to their primitive types. That's where ToPrimitive
comes into play.
The return type is now quite complex. TypeScript is not able to correlate it to the implementation anymore. That's why the type assertion at the end is needed.
CodePudding user response:
When you return the object at the end, you can use a mapped type to create a key in an object type for each T[number]
in the argument. ['propName']
can be used to extract the key on the left-hand side, and ['propValue']` will extract the value.
const props1 = [
{ propName: 'id', propValue: 1 },
{ propName: 'name', propValue: 'John' },
{ propName: 'age', propValue: 30 }
] as const;
function createObject<T extends readonly { propName: string; propValue: unknown }[]>(propertiesObject: T) {
const obj: { [key: string]: unknown } = {};
for (const { propName, propValue } of propertiesObject) {
obj[propName] = propValue;
}
return obj as {
[K in T[number] as K['propName']]: K['propValue']
};
}
const obj1 = createObject(props1); // Returned type should be: { id: number; name: string; age: number }
If
const obj1: {
id: 1;
name: "John";
age: 30;
}
is too specific, and you want just number
and string
in the values instead, you could either change the typing of the props1
, eg
const props1 = [
{ propName: 'id', propValue: 1 as number },
{ propName: 'name', propValue: 'John' as string },
{ propName: 'age', propValue: 30 as number }
] as const;
Or create a utility type that widens strings to string
and numbers to number
.
type Widen<T extends unknown> =
T extends string ? string
: T extends number ? number
// etc
: T;
return obj as {
[K in T[number] as K['propName']]: Widen<K['propValue']>
};