I've been struggling with this for hours now and am hoping someone here can help. I've scoured the internet and asked on Discord with no success. I'm trying to create a generic function that accepts an object, a key of that object that specifically holds an array value, and a new key that will be used in the transformation. A specific example of what I'm trying to do will help. I have a list of contacts of a particular shape and would like to map this to a new list of similar structures:
type PhoneNumber = {
label: string
number: string
}
type Contact = {
givenName: string
phoneNumbers: PhoneNumber[]
}
type NewContact = {
givenName: string
phoneNumber: PhoneNumber
}
const contacts: Contact[] = [ ... ]
const mappedContacts: NewContact[] = contacts.map(transform).flat()
Now I can achieve this trivially in a non-generic way:
const transform = (contact: Contact): NewContact[] => {
const { phoneNumbers, ...rest } = contact
return phoneNumbers.map(phoneNumber => ({...rest, phoneNumber })
}
but what I'm hoping for is a generic function. This obviously doesn't work, but something like this is what I've been trying:
const arrayFrom = <T extends object, K extends keyof T>(key: K, newKey: string) => (obj: T) => {
const { [key]: values, ...rest } = obj
return (values as unknown as unknown[]).map((value) => ({ ...rest, [newKey]: value }))
}
Desired usage would be something like:
const mappedContacts = contacts.map(arrayFrom('phoneNumbers', 'phoneNumber')).flat()
with the goal that mappedContacts is of type (Omit<Contact, 'phoneNumber'> & { phoneNumber: PhoneNumber })[]
but instead it is of type (Omit<Contact, 'phoneNumber'> & { [x: string]: unknown })[]
. The code works as intended, but is not type safe. The issues I know of that I can't resolve are:
- in
arrayFrom
, ts does not know thatT[K]
will be a list type, sovalues.map
creates an error without a cast to unknown[] - ts only knows
newKey
is a string so of course the right side of the intersection becomes{[x: string]: unknown}
since the above issue requires the cast.
Is it possible to tell typescript that the key I pass in will map to a list type so the cast is unnecessary? This would at least then result in the final type being: (Omit<Contact, 'phoneNumber'> & { [x: string]: PhoneNumber })[]
, which gets me closer. Secondly, is it possible to not have the right side be [x: string]
and instead have typescript infer the literal value? Many thanks in advance, I don't think this should be impossible in typescript but even lodash.set
seemingly couldn't figure out the second part as _.set({a: 4}, 'b', 'abc')
results in type {a: number}
.
CodePudding user response:
Pretty close; I think a simple cast could do it justice:
return (
values as unknown as unknown[]
).map((value) => ({ ...rest, [newKey]: value } as Omit<T, K> & { [_ in K]: (T[K] & unknown[])[number] }));
Since you can't use computed property names in types, you have to use a mapped type here instead.
CodePudding user response:
@catgirlkelly's answer got me here so I give her credit but I modified her answer to get the correct new key added (the original re-adds the omitted key):
const arrayFrom = <T, K extends keyof T, N extends string>(key: K, newKey: N) => (obj: T) => {
const { [key]: values, ...rest } = obj;
if (!Array.isArray(values)) throw new Error("obj[key] must be an array")
return values.map((value) => ({ ...rest, [newKey]: value } as Omit<T, K> & { [_ in N]: (T[K] & unknown[])[number] }));
};
I also added the array check to avoid the casts to unknown.