This is how TS unions work. It is by design.
type Option1 = {
items: string[];
}
type Option2 = {
delete: true;
}
type Combined = Option1 | Option2;
type Keys = keyof Combined; // never
As you see keyof Combined
returns never
- empty set of keys.
Because Option1
and Option2
does not have any props in common, TS is unsure what property is allowed.
Let's say you have a function which expects either Option1
or Option2
. In order to use Combined
in a safe way you should use custom typeguards:
type Option1 = {
items: string[];
}
type Option2 = {
delete: true;
}
type Combined = Option1 | Option2;
type Keys = keyof Combined; // never
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
const handle = (union: Combined) => {
if (hasProperty(union, 'items')) {
const option = union; // Option1
} else {
const option = union; // Option2
}
}
Or you can add common property:
type Option1 = {
tag: '1',
items: string[];
}
type Option2 = {
tag: '2',
delete: true;
}
type Combined = Option1 | Option2;
type CommonProperty = Combined['tag'] // "1" | "2"
const handle = (union: Combined) => {
if(union.tag==='1'){
const option = union // Option1
}
}
Another, alternative way is to use StrictUnion
.
type Option1 = {
items: string[];
}
type Option2 = {
delete: true;
}
type Combined = Option1 | Option2;
// credits goes https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type Union = StrictUnion<Combined>
const items_variable: Union['items'] = ["a", "b"]; // string[] | undefined
As you might have noticed, there is a small drawback, items_variable
might be also undefined
. This is because of union nature. It can be either one value or another.