Lately I'm trying out typescript and while diving into it I was stuck at one place where I wasn't able move forward. So I have an array of strings and I want to reduce it into an object while getting all proper hinting and code completion. Although I've arrived to a place where typescript isn't throwing any error but still I wasn't able to achieve the code completion behaviour. Pasting out the sample code here for reference.
interface GetObjI {
[key: string]: string;
}
function getObj(myArray: string[]): GetObjI {
return myArray.reduce((prev, curr) => {
prev[curr] = curr;
return prev;
}, {});
}
const arr = ["name", "age"];
const obj = getObj(arr);
console.log(obj.age); // Here I wasn't able to achieve code completion.
With strict TypeScript settings, the code you have should be throwing an error.
prev[curr] = curr;
throws
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
No index signature with a parameter of type 'string' was found on type '{}'.(7053)
Which makes sense - you aren't providing any special type for the accumulator when you're passing it in, so assigning to a property of it doesn't mesh with its existing type (as an empty object).
Use a generic type argument to indicate the type of the accumulator, and make getObj generic too so that the values in myArray can be passed along (rather than using just string[], which is too general).
You also need to type the array passed in as const so it doesn't get widened to string[] before the function call.
function getObj<T extends readonly string[]>(myArray: T) {
// Type out the type of the full object with all properties here to be DRY
type Accum = Record<T[number], T[number]>;
// The object doesn't have all properties inside the callback, though
// so we need to use Partial to indicate that some or all can be missing
return myArray.reduce<Partial<Accum>>((prev, curr: T[number]) => {
prev[curr] = curr;
return prev;
}, {}) as Accum; // When returning, indicate that it's no longer Partial, but has all properties
}
const arr = ["name", "age"] as const;
const obj = getObj(arr);
console.log(obj.age);
But .reduce is not the right tool for this situation - it requires verbose TypeScript typing and is not a great idea even in standard JavaScript because the accumulator remains the same object. Just use a loop.
function getObj<T extends readonly string[]>(myArray: T) {
const obj: Record<string, string> = {};
for (const prop of myArray) {
obj[prop] = prop;
}
return obj as Record<T[number], T[number]>;
}
Or use Object.fromEntries
const getObj = <T extends readonly string[]>(myArray: T) =>
Object.fromEntries(myArray.map(prop => [prop, prop])) as Record<T[number], T[number]>;
Unfortunately, all of these approaches do require helping TS along with some type assertions. Using a loop or reduce requires the properties to be typed as optional inside the loop, but required outside of it - and Object.fromEntries only gives a type of Record<string, T> in response, which isn't quite specific enough.