I want to build a type that requires certain properties to be present in case another property is present.
To do so I thought of using an intersection of union types. The union types are either a combination of some property and it's requirements or an empty type.
The following code does not work because it doesn't error out although the desc
property is missing:
export type A = {
a: string;
desc: string;
};
export type B = {
b: string;
} & (A | {});
const v: B = {
b: "",
a: "123"
};
Replacing {}
by Record<string, never>
results in errors because all properties are now of type never
.
export type A = {
a: string;
desc: string;
};
export type B = {
b: string;
} & (A | Record<string, never>);
const v: B = {
b: "",
a: "123"
};
Is there a way to make this concept work? Or maybe a better way?
Your approach is conceptually correct - you want an union of A
and a type that has no common keys with A.
Unfortunately sth akin to (A | {})
won't work - TS types allow extra properties.
TS has excess property check for object literals, but it does not work {}
unions.
const foo: {} = {a: 1}
ts-eslint even has a rule preventing its use:
Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
- If you want a type meaning "empty object", you probably want `Record<string, never>`
A | Record<string, never>
works fine to alone, but, as you noticed, does not work as a part of an intersection with {b: string;}
- Record forces b
to be never.
You can use following type that expresses A or sth having no keys of A
export type A = {
a: string;
desc: string;
};
type NoneOf<T> = {
[K in keyof T]?: never
}
export type B = {
b: string;
} & (A | NoneOf<A>);
const v: B = {
b: "",
a: "123"
};
There is related request for Exact Types #12936 - unfortunately not implemented for years.