Consider next example:
type Result = {
prop1?: string | undefined;
prop2?: number | undefined;
} | {
prop3?: string | undefined;
}
type Keys = keyof Result // never
There is no common keys in above union, hence TS is unable to figure out which keys it should return to you. That's why you are receiving an error.
To handle such kind of unions, you can use @Shivam Singla 's way or this generic way:
// credits goes to Titian Cernicova-Dragomir
//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>
So, instead of writing:
keyof Groups[keyof Groups];
You can write:
keyof StrictUnion<Groups[keyof Groups]>
Here is my full solution:
export type Groups = {
group1: {
prop1?: string;
prop2?: number;
};
group2: {
prop3?: string;
};
};
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>
interface AllGroups {
groupName: keyof Groups;
prop: keyof StrictUnion<Groups[keyof Groups]>;
values: string[];
};
const allGroups = <T extends AllGroups, U extends {
0: ReadonlyArray<T>,
}[
T['groupName'] extends keyof Groups
? T['prop'] extends keyof Groups[T['groupName']]
? 0 : never : never]>(arr: ReadonlyArray<T> & U) => arr;
const result = allGroups(
[{
groupName: 'group1',
prop: 'prop2',
values: ['a']
},
{
groupName: 'group1',
prop: 'prop1',
values: ['a']
}]) // ok
const result2 = allGroups(
[{
groupName: 'group1',
prop: 'prop2',
values: ['a']
},
{
groupName: 'group1',
prop: 'prop3', // expected error
values: ['a']
}]) // ok
Playground
Drawbacks: here you should use function which literally do nothing except type infering.
I assume it will be optimized by V8 engine.
You can also use closure webpack plugin to get rid of function call.
However, I'm voting for @Shivam Singla 's solution because there is no function overhead.
I just published this solution to give you some alternative way.
This type is a bit complicated:
const allGroups = <T extends AllGroups, U extends {
0: ReadonlyArray<T>,
}[
T['groupName'] extends keyof Groups
? T['prop'] extends keyof Groups[T['groupName']]
? 0 : never : never]>(arr: ReadonlyArray<T> & U) => arr;
But this is the way how I do my validation of function arguments.
If you are interesting in explanation, please ping me, I will update the answer