How to create an object type from tuple of objects?
Asked Answered
E

2

6

From this data structure :

const properties = [
  { name: 'name', type: '' },
  { name: 'age', type: 0 },
  { name: 'sex', type: ['m', 'f'] as const },
  { name: 'username', type: '' }
]

I am trying to construct -dynamically- the following type :

type Person = {
  name: string;
  age: number;
  sex: 'm'|'f';
  username: string;
}

I know TypeScript has the means to create types by mapping other types. But here the source Object is an Array so it seems a little bit tricky. How would I do that?

Errol answered 5/3, 2021 at 23:9 Comment(1)
I think that you will have to write a script that transforms a properties array into a type declaration and put it in a .d.ts file.Cherubini
I
7

A mapped type is indeed the way to go. To ensure no types get widened you will have to add as const to properties:

const properties = [
  { name: 'name', type: '' },
  { name: 'age', type: 0 },
  { name: 'sex', type: ['m', 'f'] as const },
  { name: 'username', type: '' }
] as const

You can then use a mapped type in which you remap the keys from array indices to the property names and map the values to the types. The mapped type itself looks like this:

type FromProperties<P extends readonly unknown[]> = {
  [K in IndexKeys<P> as Name<P[K]>]: Type<P[K]>
} 

It maps over the keys that are actual indices ('0', '1', etc.) rather than array properties, remaps the key to the property name, and has Type<P[K]> as the value.

To get the proper index keys, you can exclude the properties on [] from your array, so only the indices will be left:

type IndexKeys<A extends readonly unknown[]> = Exclude<keyof A, keyof []>

To get the name, a simple conditional type can be used:

type Name<O> = O extends { name: infer N } ? N extends string ? N : never : never

And to determine the actual type for each type value, you can use a slightly more complicated conditional type:

type Type<O> = 
  O extends { type: infer T }
  ?   T extends number ? number 
    : T extends string ? string 
    : T extends readonly unknown[] ? T[number]
    : never // `name` property is not a number, string or array
  : never // object has no `type` property

The extends clauses are to prevent inferring types that are too narrow (e.g. '' instead of string) and to infer the union type for arrays. You can add more types if needed.

With these definitions, Properties can be constructed with

type Person = FromProperties<typeof properties>
// Inferred type:
// type Person = {
//     name: string;
//     age: number;
//     sex: "m" | "f";
//     username: string;
// }

TypeScript playground

Identical answered 6/3, 2021 at 0:42 Comment(0)
A
1

This is actually not that hard. All you need is a mapped type with keys remapped to the type of the name property of the tuple members (which can be done very nicely with key remapping since 4.1) and values to the type property of corresponding members.

First, you need to let the compiler know the properties array is actually a tuple with an as const assertion:

const properties = [
  { name: 'name', type: '' },
  { name: 'age', type: 0 },
  { name: 'sex', type: ['m', 'f'] as const },
  { name: 'username', type: '' }
] as const;

Next, let's make the type reusable by defining a generic type able to operate on arbitrary properties of the members of the tuple. First, we need to extract indices to be able to map members to properties one-to-one. This is done via Exclude<keyof T, keyof readonly any[]> (leaves indices only).

We can use the resulting union of indices to do the mapping:

type TupleToProps<T extends readonly any[], VP extends keyof T[number], VV extends keyof T[number]> = {
    [ P in Exclude<keyof T, keyof readonly any[]> as T[P][VP] & string ] : T[P][VV] 
};

VP and VV ensure the utility type can work with any homogenous tuple of objects and & string leaves only string-compatible properties. This already yields us a pretty nice result:

type PersonTest = TupleToProps<typeof properties, "name", "type">;
/**
 * type PersonTest = {
 *  name: "";
 *  age: 0;
 *  sex: readonly ["m", "f"];
 *  username: "";
 * }
 */

After that, this is only a matter of cosmetic changes: extracting values from tuple-like types and widening literal ones (courtesy of this and this Q&A with a small upgrade to exclude keys that should not be widened):

type ToPrimitive<T> =
  T extends string ? string
  : T extends number ? number
  : T extends boolean ? boolean
  : T;
// mapped types which will preserve keys with more wide value types
type Widen<O, E = never> = {
  [K in keyof O]: K extends E ? O[K] : ToPrimitive<O[K]>
}

type TupleToPropsTwo<T extends readonly any[], VP extends keyof T[number], VV extends keyof T[number], E extends T[number][VP]> = Widen<{
    [ P in Exclude<keyof T, keyof readonly any[]> as T[P][VP] & string ] : T[P][VV] extends readonly any[] ? T[P][VV][number] : T[P][VV]
}, E>;

type PersonTest2 = TupleToPropsTwo<typeof properties, "name", "type", "sex">;
/**
 * type PersonTest2 = {
 *  name: string;
 *  age: number;
 *  sex: "m" | "f";
 *  username: string;
 * }
 */

Playground

Amphetamine answered 6/3, 2021 at 0:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.