I have this nice function which turns an object into a select option for me:
type OptionValue = string;
type OptionLabel = string;
export type Option<V extends OptionValue = OptionValue, L extends OptionLabel = OptionLabel> = {
value: V;
label: L;
};
type ExtractStrings<T> = Extract<T, string>;
export const toOption =
<
ValueKey extends string,
LabelKey extends string | ((item: ObjectIn) => OptionLabel),
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKey: ValueKey,
labelKey: LabelKey,
objectIn: ObjectIn | null | undefined
): Option<string, string> | null => {
return null // The implementation is not important here
};
const myObj = { x: "foo", y: "bar" } as const
const result = toOption("x", (params) => `${params.x} (${params.y})`, myObj)
const result2 = toOption("x", "y", myObj)
The typings work nicely, and I like how TypeScript enforces these three constrains for me:
ValueKeymust exist onObjectInLabelKeymust exist onObjectIn, if it is a string- If
LabelKeyis a function, its parameter is correctly infered toObjectIn
I would now love to take the same function, and curry the last parameter - but I run into obvious problems when typing it. If I leave all generic parameters within the outer function, I no longer get type inference for ObjectIn:
export const toOption =
<
ValueKey extends string,
LabelKey extends string | ((item: ObjectIn) => OptionLabel),
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(
valueKey: ValueKey,
labelKey: LabelKey,
) => (objectIn: ObjectIn | null | undefined): Option<string, string> | null => {
return null // The implementation is not important here
};
const myObj = { x: "foo", y: "bar" } as const
const result = toOption("x", (params) => `${params.x} (${params.y})`)(myObj)
// Property 'y' does not exist on type 'Record<"x", string>' ^^^
And I cannot move the ObjectIn, because then it is not defined when being referenced inside LabelKey:
export const toOption =
<
ValueKey extends string,
LabelKey extends string | ((item: ObjectIn) => OptionLabel),
// ^^^
// Cannot find name 'ObjectIn'. Did you mean 'Object'?
>(
valueKey: ValueKey,
labelKey: LabelKey,
) => <
ObjectIn extends Record<ExtractStrings<ValueKey | LabelKey>, OptionValue>
>(objectIn: ObjectIn | null | undefined): Option<string, string> | null => {
return null // The implementation is not important here
};
Is there something I can do to keep enforcement of the three constrains I specified earlier, while keeping the function curried?
I believe it cannot be done through direct inference like in my non-curried example, since the outer function is independent, and has no possible way to infer anything from an argument that may or may not be later supplied to its returnee. But maybe there is some other mechanism which can achieve this.
I would be fine with a version which would force me to specify the ObjectIn type explicitly when LabelKey extends Function, while still keeping the inference when LabelKey extends string:
// Would throw error when `myObj` is not assignable to `ExplicitlySpecified`
const result = toOption("x", (p: ExplicitlySpecified) => `${p.x} (${p.y})`)(myObj)
const result2 = toOption("x", "y")(myObj)
I got somewhat close, by isolating the constrain into a third function (sigh) - but I don't really like the third function, and now I run into the problem of ObjInConstrain being defined before ValueKey.
What are my options?
Thank you for your time
This may help out - I think your current method may be trying to solve multiple problems: currying (in your case is actually partial application technically) and your implementation.
Instead of having the curry logic within your method, have well defined curry methods that will do it for you. Here is the playground.
First, lets define some curry types and methods.
Now lets normalize your method to be applied normally.
At this point, let the curry functions do the work for you!