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"]>;
You essentially want a type that looks like this:
A union consisting of all valid object types.
We can build this union recursively.
This will give us the correct behavior on the tests you specified:
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:
To fix this, we have to modify our union to also forbid excess properties.
BestTypewill now look like this:This now passes our excess property test case.
Playground