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.