I have the following interfaces:
interface Values {
height: number;
width: number;
}
interface Opts {
requiredKeys: (keyof Values)[];
customParsers: Record<string, (text: string) => unknown>;
}
I want to make it so Opts['requiredKeys']
can also take strings from the keys of Opts['customParsers']
; for example:
const opts: Opts = {
requiredKeys: [
'height',
'foo', // error! not a key of `Values` and also not defined in `customParsers`
'x', // allowed because `x` is defined in `customParsers`
],
customParsers: {
x: text => parseInt(text),
},
};
I’ve tried changing Opts['requiredKeys']
‘s type to (keyof Opts['customParsers'] | keyof Values)[]
but it didn’t work.
I believe what I am trying to do can be achieved with generics but I am not very experienced with them and don’t know how I’d implement it exactly.
You are likely to allow not only "height"
and "width"
, but also values defined as key of customParsers
.
The following snippet shows one way of achieve it:
interface Opts2<T extends string> {
requiredKeys: (keyof Values | T)[];
customParsers: Record<T, (text: string) => unknown>;
}
The downside is that you will have to specify type if you want to define them in-line:
const optsNew: Opts2<"x"> = {
requiredKeys: [
'height',
'foo', // error! not a key of `Values` and also not defined in `customParsers`
'x', // allowed because `x` is defined in `customParsers`
],
customParsers: {
x: text => parseInt(text),
},
};
This can be solved by a factory function:
function createOption<T extends string>(requiredKeys: Opts2<T>["requiredKeys"], customParsers: Opts2<T>["customParsers"]): Opts2<T> {
return { requiredKeys, customParsers };
}
const optsNew /* inferred: Opts2<"x"> */ = createOption(
[
'height',
'foo',
'x',
],
{
x: text => parseInt(text),
},
);
1
The first answer has answered most of your question, except one point you mentioned:
// error! not a key of
Values
and also not defined incustomParsers
So i came up with the solution for that problem aswell: Playground link
interface Opts<T extends string> {
requiredKeys: (keyof Values | NoInfer<T>)[]; // T will not be inferred here, instead will be inferred in the Record type
customParsers: Record<T, (text: string) => unknown>;
}
function createOption<T extends string>(requiredKeys: Opts<T>["requiredKeys"], customParsers: Opts<T>["customParsers"]): Opts<T> {
return { requiredKeys, customParsers };
}
const optsNew = createOption(
[
'x',
'foo' // Type '"foo"' is not assignable to type 'keyof Values | "x"'
],
{
x: (text) => parseInt(text),
},
);