Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading