Trying to create a function where (from a typing perspective) based on the first two arguments it determines whether a 3rd argument should be provided or not. Whether the 3rd argument is required is based on if a specific nested key exists in the object based on the path of the first two arguments. I created a contrived example to demonstrate:
type ExampleType = {
john: {
toolbox: { color: string }
},
jane: {
screwdriver: { color: string }
},
sally: {
hammer: { color: string },
nail: {}
}
}
type ColorArgs<Color> = Color extends never ? { color?: Color } : { color: Color };
type ExampleFn = <
Name extends Extract<keyof ExampleType, string>,
Thing extends Extract<keyof ExampleType[Name], string>,
Color extends (ExampleType[Name][Thing] extends { color: infer C } ? C : never)
>(config: { name: Name, thing: Thing } & ColorArgs<Color>) => string;
export const exampleFn: ExampleFn = ({ name, thing, color }) => {
console.log(name, thing, color);
return 'hello';
};
exampleFn({
name: 'sally',
thing: 'nail',
});
I'm expecting the function to work correctly and not allow a color argument when name is sally and thing is nail.
Firstly, there's no real reason for this example that you'd need to have three generic type parameters in your function. Instead of using
Coloras a type parameter constrained to some type, you can just use that type instead:Then we need to fix the complicated
ColorArgs<( ExampleType[K1][K2] extends { color: infer C } ? C : never )>to be something that actually works. YourColorArgstype function is a distributive conditional type, and those always mapnevertoneverin order to be consistent with unions (see the answer to Inferring nested value types with consideration for intermediate optional keys for more information). Since you're apparently trying to useneveras a sigil to detect whencoloris not a key of the input, then you're gettingneverout instead of your intended type.So the best way forward here is to completely avoid the intermediate
never, and just handle both cases separately. I'd do this:Here I'm passing in the full
ExampleType[K1][K2]forT, and thenColorArgschecks ifcoloris a key ofTor not. If it is, you getPick<T, "color">to give you the part ofTwith that key, and if not, you get{color?: never}to prohibit such a property (it technically makes it an optional property of typenever, which is somewhat different, but very close to being a prohibition).The reason I include
color?: unknownin the type ofconfigis just to make sure the compiler knows that it's always safe to index intoconfigwithcolorno matter what generic typesK1andK2are. Otherwise you'd run into a problem here:where the destructured
colorproperty wouldn't be recognized as valid.And now we can check that it works as desired:
Looks good.
Playground link to code