I have the following types and function:
interface Values {
width: number;
height: number;
};
interface Opts<T extends string> {
requiredKeys: (keyof Values | NoInfer<T>)[];
customParsers: Record<T, (text: string) => unknown>;
}
function example<T extends string>(opts: Partial<Opts<T>>): Partial<Values>;
I am trying to make it so they keys from Opts['customParsers']
also appear on the return type of example, extending Values
; for example:
example({
customParsers: {
x: text => parseInt(text),
},
}; // returns { width?: number, height?: number, x?: number }
I am not sure how I'd go about this. I've tried the following which unfortunately didn't work:
type Values<T extends Opts<string>> = {
width: number;
height: number;
} & {
[K in keyof T]: ReturnType<T[K]>;
};
In order to keep track of this you need Opts
to maintain a relationship between each key of customParsers
and the corresponding return type of the method. That means you will probably want Opts
to be generic in an object type T
, like this:
interface Opts<T extends object> {
requiredKeys: (keyof (Values & T))[];
customParsers: { [K in keyof T]: (text: string) => T[K] }
}
declare function example<T extends object>(
opts: Partial<Opts<T>>
): Partial<Values & T>;
Here we can see that customParsers
is a mapped type over T
, so that each property of customParsers
at key K
is a method that accepts a string
input and produces a T[K]
output.
And requiredKeys
is an array of element type keyof (Values & T)
, where the intersection Values & T
is effectively the output type of example()
, and so requiredKeys
should be some set of keys of that type.
Note that I removed the use of the NoInfer
utility type because it isn't strictly necessary (you will get errors if requiredKeys
contains elements not present as keys of customParsers
or "width"
or "height"
) and because keeping it there actually prevents inference you need. Maybe it should stay there and someone should file an issue about NoInfer
in the TypeScript repo, but that's probably out of scope for this question.
Let's test it:
const ret = example({
requiredKeys: ['x'],
customParsers: {
x: text => parseInt(text),
},
});
ret.height?.toFixed(1);
ret.width?.toFixed(1);
ret.x?.toFixed(1);
Looks good. The type of T
is inferred as {x: number}
, and so the output type Partial<Values & {x: number}>
behaves as desired.