I'm trying to reduce amount of code and error by introducing static constraints to chat commands. Commands are described with a name and arguments. Here's what I've got:
type ArgumentTypeName = 'string' | 'number' | 'user';
type CommandArguments<T> = { [k in keyof T]: ArgumentTypeName };
type CommandHandler<T> = (args: T) => void;
type Command<T> = {
name: string;
arguments: CommandArguments<T>;
handler: CommandHandler<T>;
}
I can't figure out how to get T
type from arguments
of Command<T>
, so it can correctly influence handler
.
So far I'm getting a type with correct keys, but all values are unknown
. Like this:
arguments = { arg1: 'string' }
// handler: (args: { arg1: unknown }) => void
I also tried to constraint arguments
, thinking that if I don't specify T, it will be inferred from arguments
, but that didnt work:
type ArgumentName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends User ? 'user' :
never;
type CommandArguments<T> = { [k in keyof T]: ArgumentName<T[k]> };
type CommandHandler<T> = (args: T) => void;
type Command<T> = {
name: string;
arguments: CommandArguments<T>;
handler: CommandHandler<T>;
}
How can I, like, infer-backwards T
based on its property CommandArguments<T>
?
So I get CommandArguments<T>
→ T
→ CommandHandler<T>
?
Or is there a different approach that I didn't think of?
Edit: Example of code that uses this:
/*
In another file:
const commands: Command<any>[] = [];
export function createCommand<T>(cmd: Command<T>) {
commands.push(cmd);
}
*/
createCommand({
name: 'add',
arguments: {
a: 'number',
b: 'number'
},
handler: (args) => {
// (parameter) args: { a: unknown; b: unknown; }
// Error: Object is of type 'unknown'. ts(2571)
console.log(args.a + args.b);
},
});
To infer CommandHandler
from CommandArguments
, we probably don't want to do the opposite, i.e. ArgumentName<T[k]>
in
type CommandArguments<T> = { [k in keyof T]: ArgumentName<T[k]> };
Another problem with the above approach is that from the above declaration, CommandArguments
only constraints the keys of T
, that's why T
's properties value type are inferred to be just unknown
.
A straightforward solution is to constraint arguments
's type T
to extends an object of ArgumentTypeName
, then infer handler
's args
from that type T
. Here we need to use T extends CommandArguments
instead of CommandArguments
in order to capture the actual usage type.
declare interface User{};
type ArgumentType<T> =
T extends 'string' ? string :
T extends 'number' ? number :
T extends 'user' ? User :
never;
type ArgumentTypeName = 'string' | 'number' | 'user';
type CommandArguments = { [k: string]: ArgumentTypeName };
type CommandHandler<T> = (args: { [k in keyof T]: ArgumentType<T[k]>}) => void;
type Command<T extends CommandArguments> = {
name: string;
arguments: T;
handler: CommandHandler<T>;
}
const commands: Command<any>[] = [];
export function createCommand<T extends CommandArguments>(cmd: Command<T>) {
commands.push(cmd);
}
createCommand({
name: 'add',
arguments: {
a: 'number',
b: 'string',
c: 'user',
},
handler: (args) => {
// (parameter) args: { a: number; b: string; c: User }
console.log(args.a.toPrecision(2) + args.b.padStart(30));
},
});
Playground Link
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With