Home > Back-end >  How can I create a TypeScript type that reduces an array of const objects to a const record object?
How can I create a TypeScript type that reduces an array of const objects to a const record object?

Time:11-09

I want to write a TypeScript type that "reduces" an input array of objects to a record object, with the object keys taken from a property of the input array members.

This will be used for const inputs and therefore the resulting object should have correct types for each member.

This is my current attempt, but the ExtractName<Args[N]> is not working.

How can I solve this?

type TypedArguments<Args extends readonly ArgumentSpec[]> = {
  // what I would like is to output an object type with
  // fixed string keys taken from each member of the (const) input
  readonly [N in keyof Args as ExtractName<Args[N]>]: ExtractValue<Args[N]>

  // this works but output is an array
  // readonly [N in keyof Args]: ExtractValue<Args[N]>
};

type ExtractName<
  T extends ArgumentSpec
> = T['name']

type ExtractValue<
  T extends ArgumentSpec
> =
  T['multiple'] extends true ? string[] : string

export interface ArgumentSpec {
  name: string
  multiple: boolean
}

function mapArguments<Args extends readonly ArgumentSpec[]>(args: Args): TypedArguments<Args> {
    // content here is not important, this would do some parsing of
    // input and then do the as TypedArguments<Args> at the end
    return args.reduce(
      (arg: ArgumentSpec, agg) => ({ ...agg, [arg.name]: arg.multiple ? ['foo', 'bar'] : 'baz' }),
      {} as any
    ) as unknown as TypedArguments<Args>
}

const mapped = mapArguments([
    { name: 'key1', multiple: true },
    { name: 'key2', multiple: false }
] as const)

// Now I want this to work
mapped.key1.map((x: string) => x.toUpperCase())
mapped.key2.toUpperCase()


// when using the commented-out line in the TypedArguments type instead,
// then this works. I'd like an object though, not an array.
// mapped[0] is string[]
// mapped[1] is string
// mapped[0].map(x => x.toUpperCase())
// mapped[1].toUpperCase()


See also in Typescript Playground

Expected this to work as outlined above. Did not find a TypeScript construct that would make it work.

CodePudding user response:

The issue here is the way you are trying to use a mapped type to "iterate" over a tuple but you also want an object type as the result. keyof Args will produce not only the elements but also all the other properties that arrays can have and that messes with your return type.

Intead, map over the union of elements with Args[number].

type TypedArguments<Args extends readonly ArgumentSpec[]> = {
  readonly [N in Args[number] as N["name"]]: ExtractValue<N>
};

Also, I used the Expand utility type so we can properly see the expanded return type of the function.

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

function mapArguments<Args extends readonly ArgumentSpec[]>(args: Args): Expand<TypedArguments<Args>> {
    // content here is not important, this would do some parsing of
    // input and then do the as TypedArguments<Args> at the end
    return args.reduce(
      (arg: ArgumentSpec, agg) => ({ ...agg, [arg.name]: arg.multiple ? ['foo', 'bar'] : 'baz' }),
      {} as any
    ) as unknown as Expand<TypedArguments<Args>>
}

Playground

  • Related