Consider this example:
export enum EId {
userId = 'userId',
customerId = 'customerId',
}
type AtLeastOne<Obj, Keys = keyof Obj> = Keys extends keyof Obj ? Record<Keys, string> : never
// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
export type IIdParamString =
& StrictUnion<AtLeastOne<typeof EId>>
& {
paramString: string;
}
/**
* Ok
*/
const params: IIdParamString = {
userId: '1234',
paramString: 'limit',
};
/**
* Expected errors
*/
const params2: IIdParamString = {
paramString: 'limit',
};
const params3: IIdParamString = {
userId: '1234',
customerId: 'sdf',
paramString: 'limit',
};
AtLeastOne
- you can find full explanation here
StrictUnion
- you can find full explanation here. This utility makes similar trick with never
which you did.
Is my approach in general conventional to my problem, to make sure it is either this or that
Yes, your approach is ok. But I recommend you to use discriminated unions. They are also known as tagged unions
. For instance see F# discriminated unions. As you might have noticed, each union has own tag/flag/marker. It helps compiler to distinguish them.
Could it be an interface ?
Unfortunately - no. It could be only type, because interface
can extends only statically known type. See example:
// An interface can only extend an object type or intersection of object types with statically known members.
export interface IIdParamString extends StrictUnion<AtLeastOne<typeof EId>> { // error
paramString: string;
}
Regarding your last question, I'm not sure if I understand it.
You can update your question or (the better) option to ask separate.