I have a type like
type WeakGuaranteeType = {
a?: string,
b?: string,
c?: string,
d?: string,
}
But, I want to make a stronger guarantee. I know that a
will always exist when b
exists, and b
will always exist when c
exists, etc. So:
{ a: "" } // valid
{ a: "", b: "" } // valid
{ a: "", b: "", c: "" } // valid
{ c: "" } // not valid!!
Is there a way to describe this in TypeScript?
My first attempt was to do:
type BetterType =
| {}
| ({a: string} & (
| {}
| ({b: string} & (
| {}
| ({c: string} & (
// etc...
))
))
))
This is bad because:
- It is very verbose and has to be written out by hand, and
- It produced an
"Expression produces a union type that is too complex to represent"
error.
I think it would be better to use a recursive approach.
type RecursiveRequire<Keys extends string[]>
= Keys extends [] // Check if Keys is empty
? {} // Base case. If Keys is empty, "return" {}
: ( // Recursive case. Add the first key and recurse on the rest
{ [Key in Keys[0]]: string } &
RecursiveRequire</* What do I put here to say "all of Keys except the first"? */>
);
type BestType = RecursiveRequire<["a", "b", "c", "d"]>;
CodePudding user response:
You essentially want a type that looks like this:
type BestType = {
a: string;
b: string;
c: string;
d: string;
} | {
a: string;
b: string;
c: string;
} | {
a: string;
b: string;
} | {
a: string;
}
A union consisting of all valid object types.
We can build this union recursively.
type RecursiveRequire<Keys extends string[]> = Keys extends [
...infer E extends string[],
infer R extends string
]
? { [Key in E[number] | R]: string } | RecursiveRequire<E>
: never;
type BestType = RecursiveRequire<["a", "b", "c", "d"]>;
This will give us the correct behavior on the tests you specified:
const a: BestType = { a: "" }; // valid
const b: BestType = { a: "", b: "" }; // valid
const c: BestType = { a: "", b: "", c: "" }; // valid
const d: BestType = { c: "" }; // not valid!!
But there is a catch. While our union represents all valid object types, it does not in any way disallow invalid combinations. Remember: excess properties are mostly allowed in TypeScript's structural type system.
So this is not an error:
const e = { c: "", a: "" }
const f: BestType = e
To fix this, we have to modify our union to also forbid excess properties.
type RecursiveRequire<
Keys extends string[],
N extends string = never
> = Keys extends [
...infer E extends string[],
infer R extends string
]
? (({
[Key in E[number] | R]: string
} & {
[K in N]?: never
}) | RecursiveRequire<E, R | N>) extends infer U ? {
[K in keyof U]: U[K]
} : never
: never;
type BestType = RecursiveRequire<["a", "b", "c", "d"]>;
BestType
will now look like this:
type BestType = {
a: string;
b: string;
c: string;
d: string;
} | {
a: string;
b: string;
c: string;
d?: undefined;
} | {
a: string;
b: string;
c?: undefined;
d?: undefined;
} | {
a: string;
b?: undefined;
c?: undefined;
d?: undefined;
}
This now passes our excess property test case.
const a: BestType = { a: "" }; // valid
const b: BestType = { a: "", b: "" }; // valid
const c: BestType = { a: "", b: "", c: "" }; // valid
const d: BestType = { c: "" }; // not valid!!
const e = { c: "", a: "" }
const f: BestType = e // not valid!!