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

Time:05-04

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}.

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.

Playground

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.

  • Related