Jack
Jack

Reputation: 1076

TypeScript: How can I extract from a type only the keys and values of type array

I'd like to come up with some conditional to extract only the properties from an object whose value is an array.

For example:

type Person = {
   name: string
   addresses: Address[]
   age: number
   phoneNumbers: PhoneNumber[]
}

PullOutArrays<Person> => {
   phoneNumbers: PhoneNumber[]
   addresses: Address[]
}

I attempted something like this to no avail:

type PulledOutArrays<T extends Record<string, unknown>> = {
  [K in keyof T]: T[K] extends unknown[] ? T[K] : never
}

Upvotes: 3

Views: 3043

Answers (3)

akuiper
akuiper

Reputation: 214927

Or if you are using TS 4.1+, a more direct approach could be to remove keys by asserting the key as never based on the value type:

type OnlyArraysPerson = {
  [K in keyof Person as Person[K] extends unknown[] ? K : never]: Person[K]
}

To be more generic:

type OnlyArrays<T> = {
 [K in keyof T as T[K] extends unknown[] ? K : never]: T[K]
}

type ArrayPerson = OnlyArrays<Person>

Playground

Upvotes: 2

lawrence-witt
lawrence-witt

Reputation: 9354

Cribbing from this GitHub issue:

type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];

type FilteredProperties<T, U> = { [K in FilteredKeys<T, U>]: T[K]; };

type PullOutArrays<T> = FilteredProperties<T, unknown[]>;

type Person = {
  name: string
  addresses: string[]
  age: number
  phoneNumbers: number[]
}

type PersonKeys = FilteredKeys<Person, unknown[]>;

// "addresses" | "phoneNumbers"

type PersonArrays = PullOutArrays<Person>;

// {
//    addresses: string[];
//    phoneNumbers: number[];
// }

As you may have seen from your original attempt, what you end up with after mapping the conditional:

{ [P in keyof T]: T[P] extends U ? P : never }

Is an interface containing all the property keys, but with the non-array types changed to never, apparently because TS does not initially check what the types assigned to property keys are. By adding an index on the end, you force the compiler to read the value of the property and ultimately omit it from the result.

Using the union which is generated, you can then create a new mapped type from that union on the original interface.

Upvotes: 6

Jack
Jack

Reputation: 1076

Came to a solution by following this answer:

type KeysOfType<T, U, B = false> = {
  [P in keyof T]: B extends true
    ? T[P] extends U
      ? U extends T[P]
        ? P
        : never
      : never
    : T[P] extends U
    ? P
    : never
}[keyof T]

type PickByType<T, U, B = false> = Pick<T, KeysOfType<T, U, B>>

type PickArrays<T> = PickByType<T, unknown[]>

@lawrence-witt's answer is more elegant and should probably be used.

Upvotes: 1

Related Questions