Home > Enterprise >  How to use a union type with dynamic a key value in object?
How to use a union type with dynamic a key value in object?

Time:12-02

I am trying to use a union type of two strings passing in one of the strings as an argument then I would like to use this argument as the key for a object property.

type TimetabledClass = {
  classCode: string, students: string[]
} | { classCode: string, teachers: string[] }

function getTimetabledClassEnrolments(
  type: 'students' | 'teachers',
  classCode: string
) {
  const timetabledClasses: TimetabledClass[] = [] 

  const enrolments = ['person1', 'person2']

  timetabledClasses.push({
    classCode,
    [type]: enrolments // error!
    // Argument of type '{ [x: string]: string | string[]; classCode: string; }'
    // is not assignable to parameter of type 'TimetabledClass'.
  })

  return timetabledClasses
}

CodePudding user response:

Your problem is caused by a known issue in TypeScript; see microsoft/TypeScript#13948. If you have a computed property name whose type is anything but a single literal type, the compiler will widen the type of the key all the way to string, resulting in a string index signature, which is often undesirable:

function computed<K extends string>(k: K, s: string) {

    const literal = { ["a"]: 123 };
    // const literal: { a: number }

    const union = { [Math.random() < 0.5 ? "x" : "y"]: "abc" };
    // const union: { [x: string]: string } 

    const templateLiteral = { [`a${s}b`]: "abc" };
    // const templateLiteral: { [x: string]: string } 

    const generic = { [k]: 123 };
    // const generic: { [x: string]: number }

}

You can see that only literal is of a type with a specific key. The rest, are all widened to a string index signature.

In your code, then, because type's type is a union of string literals, the value { classCode, [type]: enrolments } is seen as having type { [x: string]: string | string[]; classCode: string; }, which is not assignable to TimetabledClass.


You could always just assert that what you're doing is correct:

timetabledClasses.push({
    classCode,
    [type]: enrolments
} as TimetabledClass); // okay

but it is possible to do a little better via a workaround for microsoft/TypeScript#13948. I often use a helper function called kv(k, v) which produces an object with a property of key k and value v with a more specific type:

function kv<K extends PropertyKey, V>(
    k: K, v: V
): { [P in K]: { [Q in P]: V } }[K] {
    return { [k]: v } as any
}

Yes, it uses an assertion, but the point is that this function only has to be written once but can be used in many places, which reduces the overall number of safety holes.

The return type is essentially a distributive Record<K, V> type where a union of key types will become a union of output types. Here's how it works with the examples from computed() above:

function withKv<K extends string>(k: K, s: string) {

    const literal = kv("a", 123);
    // const literal: { a: number }

    const union = kv(Math.random() < 0.5 ? "x" : "y", "abc");
    // const union: { x: string; } | { y: string; }

    const templateLiteral = kv(`a${s}b`, "abc");
    // const templateLiteral: { [x: `a${string}b`]: string } 

    const generic = kv(k, 123);
    // const generic:  { [P in K]: { [Q in P]: number; }; }[K]

}

The type of literal is the same literal type as before, but now the other values have stronger types. The union type is itself a union. The templateLiteral type is a template string pattern index signature, and the generic type is still generic.

So we can use the union support in your code to get this:

timetabledClasses.push({
    classCode,
    ...kv(type, enrolments)
}); // okay

where I'm using the spread operator to convert the output of kv() to an inline property.

That compiles without error because the type of the element is seen as

const val = { classCode, ...kv(type, enrolments) };
// const val: { students: string[]; classCode: string } | 
//  { teachers: string[]; classCode: string }

which is equivalent to TimetabledClass, as desired.

Playground link to code

  • Related