Typescript: Specify that type parameter is key of object and has a specific type

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 that T[K] will be a list type, so values.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}.

>Solution :

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.

Playground

Leave a Reply