Here is my code:
export type CommandOptionType = string | number | boolean;
export type CommandOption = Record<string, CommandOptionType>;
export type LeafCommand<T extends CommandOption> = {
name: string;
handle: (
interactionOptionValues: { [K in keyof T]: T[K] },
) => Promise<void>;
};
export type LeafCommandGroup = {
name: string;
commands: Record<string, LeafCommand<CommandOption>>;
};
export type RootCommand = {
name: string;
commands: Record<string, LeafCommandGroup | LeafCommand<CommandOption>>;
};
Usage of these types:
type OPTIONS = {
option1: "type1" | "type2" | "type3" | "type4";
option2: string;
option3: boolean;
};
const Type = {
name: "type",
handle: async (interactionOptionValues) => {
// business logic here
},
} satisfies LeafCommand<OPTIONS>;
The error occurs below code snippet:
const Root = {
name: "root",
commands: { // error occurs here
[Type.name]: Type,
},
} satisfies RootCommand;
Here, I'm getting the error which says Type { [x: string]: CommandOptionType } is missing the following properties from type. option1, option2, option3
. I don't know why Typescript complains about other index keys which are strings as well. I couldn't find a work around.
Desired result is that I should be able to add commands which has type of LeafCommand without a hustle.
If you have any other design patterns that is cleverer, I'm down for it.
Thanks for your time.
Your LeafCommand<T>
type is contravariant in T
(see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript) because T
appears as the parameter type of a function. That means LeafCommand<T> extends LeafCommand<U>
if U extends T
, and not vice versa. So since OPTIONS extends CommandOption
, you can only conclude that LeafCommand<CommandOption> extends LeafCommand<OPTIONS>
. The opposite, which is what you're doing, is not true... because it's not safe. For example:
let lcO: LeafCommand<OPTIONS> =
{ name: "", handle(o) { o.option2.toUpperCase(); return Promise.resolve() } };
let lcCO: LeafCommand<CommandOption> = lcO; // error, for good reason!
lcCO.handle({}); // error at runtime, o.option2 is undefined
If you assign a LeafCommand<OPTIONS>
to a LeafCommand<CommandOption>
, then when you call the handle()
method and give it a CommandOption
, the implementation will assume that it got an OPTIONS
, and if this assumption is wrong you can easily get runtime errors.
If you're looking for a type to which all LeafCommand<T>
s are assignable no matter what T
is, then you need a type which is assignable to all T
s. The only way that will work is to use the never
type:
type RootCommand = {
name: string;
commands: Record<string, LeafCommandGroup | LeafCommand<never>>;
};
const Root = {
name: "root",
commands: { // okay now
[Type.name]: Type,
},
} satisfies RootCommand;
This might or might not end up satisfying your underlying use case (you'll find yourself unhappy if you try to call a handle()
on LeafCommand<never>
, but that's just a consequence of contravariance), but it's at least type safe.