If you are looking for compile-time validation only, it can be achieved easily using a mapped type and an intersection (with an identity mapped type for aesthetics):
type SomeData = {
A: boolean
B: boolean
C: boolean
}
type OneTrueOnly<T extends object, K extends keyof T> = {
[P in K] : true
} & {
[P in Exclude<keyof T, K>]: false
};
type Identity<T> = { [P in keyof T] : T[P] }
//{ A: true; } & { B: false; C: false; }
type test = OneTrueOnly<SomeData, "A">;
//{ A: true; B: false; C: false; }
type pretty = Identity<test>;
If you require compile-time type guarding to narrow down types later on, a union needs to be used. The previous type can serve as a foundation for another utility type:
type MaybeOneTrue<T extends object> = {
[P in keyof T]: OneTrueOnly<T, P>
}[keyof T];
//OneTrueOnly<SomeData, "A"> | OneTrueOnly<SomeData, "B"> | OneTrueOnly<SomeData, "C">
type combo = MaybeOneTrue<SomeData>;
const process = (obj: combo) => {
//narrowing the type to a union member:
if(obj.A) {
console.log(obj); //OneTrueOnly<SomeData, "A">
}
};
If runtime checks are needed, there are options available. Below is both a type and runtime guard based on the previously defined mapped types (MaybeOneTrue
and OneTrueOnly
):
const checking = <K extends keyof combo>(obj: combo, key: K) : obj is Extract<combo, { [P in K] : true }> => {
return Object.entries(obj).every(([k,v]) => k === key || !v );
};
{
const obj:combo = { A:true, B:false,C:false };
if(checking(obj, "A")) obj //OK, obj is OneTrueOnly<SomeData, "A">
}
Playground