Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get object type based on it's property values

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>TCommandHandler<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);
  },
});
like image 206
Vlad Avatar asked Nov 06 '22 03:11

Vlad


1 Answers

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

like image 53
Phu Ngo Avatar answered Nov 15 '22 12:11

Phu Ngo