Reputation: 1173
Let's say I have an object:
type Obj = { a: string, b: string, c: string }
With Partial<T>
, TS gives you every possible combination of the object properties, including the full form of the object. In my case, I want to exclude the full object (also the empty one if possible).
So I want:
{} // ❌
{ a: string } // ✅
{ b: string } // ✅
{ c: string } // ✅
{ a: string, b: string } // ✅
{ a: string, c: string } // ✅
{ a: string, b: string, c: string } // ❌
How can I do that?
Upvotes: 4
Views: 1973
Reputation: 10365
To achieve this behavior, you need to create two constraints for the Obj
type. The first one should exclude the "full" type and the second - the "empty" type.
The first constraint means that at least one property should be of type undefined
(or not present at all). Therefore, we need a union of types where at least one of the properties conforms to the constraint.
First, we have to map the initial type to get all possible combinations of types where one property is omitted (note the ?
modifier - it ensures that a variable number of properties is allowed) and extract our union with keyof T
:
{
[ P in keyof T ] : {
[ K in Exclude<keyof T, P> ] ?: T[P]
}
}[keyof T]
If we leave it at that, we will still be allowed to specify all properties, so we need to explicitly tell the compiler to disallow the third property. ?
modifier ensures we can omit the property:
type NotAll<T> = {
[ P in keyof T ] : {
[ K in Exclude<keyof T, P> ] ?: T[P]
} & { [ M in P ] ?: never }
}[keyof T]
The second constraint means that at least one property should be defined. The logic is pretty much the same, but this time for each of the properties we have a type of all properties except one set to optional intersected with a type where this property is required:
type AtLeastOne<T> = {
[ P in keyof T ] : {
[ K in Exclude<keyof T, P> ] ?: T[P]
} & { [ M in P ] : T[M] }
}[keyof T];
finally, we need to combine the second constraint with the first. Since the first constraint gives us a union of allowed types, AtLeastOne
should be applied to each member of the union:
type NotAll<T> = {
[ P in keyof T ] : AtLeastOne<{
[ K in Exclude<keyof T, P> ] : T[P] //note the modifier is moved to `AtLeastOne`
}> & { [ M in P ] ?: never }
}[keyof T];
That's it, let's test our type:
type test = NotAll<Obj>;
const empty : test = {}; //error
const a : test = { a: "test" }; //OK
const ab : test = { a: "test", b: "test" }; //OK
const bc : test = { b: "test", c: "test" }; //OK
const abc : test = { a: "test", b : "test", c: "test" }; //error
Upvotes: 7