Uncertain if the code below qualifies as excessively verbose (due to using Pick<>
internally), or if it encounters the same compilation issues mentioned previously, but:
type MissingOneProperty<O extends object> = {
[K in keyof O]: Pick<O, Exclude<keyof O, K>>
}[keyof O];
type MissingAtMostOneProperty<O extends object> =
O | MissingOneProperty<O>;
The concept is that
MissingAtMostOneProperty<O>
will either be
O
itself, or it will lack exactly one property from
O
. This approach likely only applies to object types without index signatures (is this important?).
If the model is defined like this:
interface Model {
a: number,
b: number,
c: number
}
A function can be declared to accept only models lacking at most one property:
declare function processModel(
validModel: MissingAtMostOneProperty<Model>
): Model;
processModel({ a: 1, b: 2 }); // acceptable
processModel({ b: 2, c: 0.5 }); // acceptable
processModel({ a: 1, c: 0.5 }); // acceptable
processModel({ a: 1, b: 2, c: 0.5 }); // acceptable
processModel({ a: 1 }); // error, missing "b" property
processModel({ b: 2 }); // error, missing "a" property
processModel({ c: 0.5 }); // error, missing "a" property
processModel({}); // error, missing "a" property
This seems logical to me.
To grasp how this functions, let's break down what
MissingAtMostOneProperty<Model>
evaluates to:
MissingAtMostOneProperty<Model>
becomes, following the definition of MissingAtMostOneProperty
:
Model | MissingOneProperty<Model>
which follows the definition of MissingOneProperty
:
Model | {[K in keyof Model]: Pick<Model, Exclude<keyof Model, K>>}[keyof Model]
then mapping over the 'a'
, 'b'
, and 'c'
properties of Model
:
Model | {
a: Pick<Model, Exclude<keyof Model, 'a'>,
b: Pick<Model, Exclude<keyof Model, 'b'>,
c: Pick<Model, Exclude<keyof Model, 'c'>
}[keyof Model]
considering that keyof Model
equals 'a'|'b'|'c'
and Exclude<T, U>
removes elements from unions:
Model | {
a: Pick<Model, 'b'|'c'>,
b: Pick<Model, 'a'|'c'>,
c: Pick<Model, 'a'|'b'>
}['a'|'b'|'c']
utilizing how Pick<>
works, we get:
Model | {
a: { b: number, c: number },
b: { a: number, c: number },
c: { a: number, b: number }
}['a'|'b'|'c']
finally, by looking up a union of property keys in a type being equivalent to a union of the types of the properties, and per the definition of Model
, results in:
{a: number, b: number, c: number}
| { b: number, c: number }
| { a: number, c: number }
| { a: number, b: number }
There you have it! The outcome is a union of Model
along with all possible ways to remove one property from Model
.
Hope this provides some insight. Best of luck!