从包含对象中推断嵌套函数的参数类型

huangapple go评论76阅读模式
英文:

Infer nested function parameter types from containing object

问题

我对TypeScript遇到的问题感到困惑。我正试图构建一个命令解析工具,并希望在配置对象中添加类型提示。

考虑以下内容:

interface Argument {
  type: 'string' | 'number'
}

interface Command {
  label: string
  arguments: Record<string, Argument>
}

// --

export default {
  label: 'execute-task',
  arguments: {
    'task_name': {
      type: 'string'
    }
  },
} as Command

现在考虑我想在这个对象中添加一个'handler'函数:

type HandlerOptions<...> = ...

interface Command {
  label: string
  arguments: Record<string, Argument>
  handler: (options: HandlerOptions<...>) => void
}

// --

export default {
  label: 'execute-task',
  arguments: {
    'task_name': {
      type: 'string'
    }
  },
  handler(options) {
    console.log(options)
  },
} as Command

我想要在上面的console.log(options)行上获得options对象的类型推断,以便:

  • 首先,typeof options解析为{ task_name: unknown }或类似的内容
  • 在更高级的版本中,它解析为{ task_name: string },尽管我希望在实现上一个目标后这将变得简单。

我遇到的问题是在上面的示例中定义HandlerOptions<...>类型,以便解析实际对象属性,而不是返回空记录的Command通用类型。

查看文档后,我一直在尝试的主要方向是以下各种变化(索引类型):

type HandlerOptions<C extends Command> = {
  [key in keyof C['arguments']]: ...
}

尝试使用ThisType<Command>,使用实用类型“重建”类型,并将配置对象包装在返回计算类型的函数中... 但对于在原始对象本身中进行类型提示没有帮助。

我最接近的尝试解析为{ [x: string]: ... }或空对象。

因此,我在思考:

  • 这是否可能?
  • 如果是的话,我漏掉了什么关键点?
  • 如果不可能,是否有类似的开发体验的可能替代方案?

谢谢!

英文:

I've got myself hitting a bit of a brick wall with Typescript. I am attempting to build a command-parsing utility, and would like to add type hints within configuration objects.

Consider the following;

interface Argument {
  type: &#39;string&#39; | &#39;number&#39;
}

interface Command {
  label: string
  arguments: Record&lt;string, Argument&gt;
}

// --

export default {
  label: &#39;execute-task&#39;,
  arguments: {
    &#39;task_name&#39;: {
      type: &#39;string&#39;
    }
  },
} as Command

Now consider that I would like to add a 'handler' function within this object;

type HandlerOptions&lt;...&gt; = ...

interface Command {
  label: string
  arguments: Record&lt;string, Argument&gt;
  handler: (options: HandlerOptions&lt;...&gt;) =&gt; void
}

// --

export default {
  label: &#39;execute-task&#39;,
  arguments: {
    &#39;task_name&#39;: {
      type: &#39;string&#39;
    }
  },
  handler(options) {
    console.log(options)
  },
} as Command

I would like to get type inference on the options object at console.log(options) line above, such that;

  • to begin with; typeof options resolves { task_name: unknown } or similar
  • in a more advanced version, it resolves to { task_name: string }, though I expect this will be simple once my previous goal is achieved.

What I'm struggling with is defining the HandlerOptions&lt;...&gt; type in the above example, such that it resolves the actual object properties, instead of the Command generic, which returns an empty record.

Looking at the docs, the primary direction I've been trying is variations of the following (indexed types);

type HandlerOptions&lt;C extends Command&gt; = {
  [key in keyof C[&#39;arguments&#39;]]: ...
}

Attempted with ThisType&lt;Command&gt;, 'rebuilding' the type using utility types, and wrapping the configuration object in a function that returns a computed type... but that doesn't help for type hinting in the original object itself.

The closest I have been with this resolves to { [x: string]: ... } or an empty object.

I'm therefore wondering;

  • Is this even possible?
  • If yes, what's the nugget that I'm missing here?
  • If not, what would be a possible alternative for similar developer experience?

Thank you!

答案1

得分: 0

以下是您要翻译的部分:

"So I think I understand what you're looking for, but I apologize if I got it wrong. I modified your types and added a generic createCommand helper method that allows the type of options to be inferred:

interface Argument {
  type: &quot;string&quot; | &quot;number&quot;;
}

type HandlerOptions&lt;Args extends Record&lt;string, Argument&gt;&gt; = {
  [key in keyof Args]: any;
};

interface Command&lt;Label extends string, Args extends Record&lt;string, Argument&gt;&gt; {
  label: Label;
  arguments: Args;
  handler: (options: HandlerOptions&lt;Args&gt;) =&gt; void;
}

function createCommand&lt;Label extends string, Args extends Record&lt;string, Argument&gt;&gt;(
  label: Label,
  args: Args,
  handler: (options: HandlerOptions&lt;Args&gt;) =&gt; void
): Command&lt;Label, Args&gt; {
  return {
    label,
    arguments: args,
    handler,
  };
}

const testCommand = createCommand(&quot;execute-task&quot;, { task_name: { type: &quot;string&quot; } }, (options) =&gt; {
  console.log(options.task_name);
});

When writing the function I passed as the handler argument to createCommand, the type of options was inferred - Intellisense suggested task_name after typing "options."."

英文:

So I think I understand what you're looking for, but I apologize if I got it wrong. I modified your types and added a generic createCommand helper method that allows the type of options to be inferred:

interface Argument {
  type: &quot;string&quot; | &quot;number&quot;;
}

type HandlerOptions&lt;Args extends Record&lt;string, Argument&gt;&gt; = {
  [key in keyof Args]: any;
};

interface Command&lt;Label extends string, Args extends Record&lt;string, Argument&gt;&gt; {
  label: Label;
  arguments: Args;
  handler: (options: HandlerOptions&lt;Args&gt;) =&gt; void;
}

function createCommand&lt;Label extends string, Args extends Record&lt;string, Argument&gt;&gt;(
  label: Label,
  args: Args,
  handler: (options: HandlerOptions&lt;Args&gt;) =&gt; void
): Command&lt;Label, Args&gt; {
  return {
    label,
    arguments: args,
    handler,
  };
}

const testCommand = createCommand(&quot;execute-task&quot;, { task_name: { type: &quot;string&quot; } }, (options) =&gt; {
  console.log(options.task_name);
});

When writing the function I passed as the handler argument to createCommand, the type of options was inferred - Intellisense suggested task_name after typing "options.".

答案2

得分: 0

目前,没有直接使用你想要的类型来完成这个操作的方法。相反,我们应该向 Command 接口添加一个泛型参数,该参数将代表 arguments

interface Command<T extends Record<string, Argument> = Record<string, Argument>> {
  label: string;
  arguments: T;
  handler: (options: HandlerOptions<T>) => void;
}

对于 HandlerOptions,我们需要一个映射,将参数的字符串文字转换为实际类型:

type TypeMap = {
  string: string;
  number: number;
};

HandlerOptions 中,我们将使用映射类型来实现所需的类型:

type HandlerOptions<T extends Record<string, Argument>> = {
  [K in keyof T]: TypeMap[T[K]["type"]];
}

示例:

// 类型示例
type Example = HandlerOptions<{ argument1: { type: "string" } }>;

我们还需要一个函数,该函数接受一个 command 并返回它。此函数的唯一目的是推断 arguments 的类型:

const getCommand = <T extends Record<string, Argument>>(command: Command<T>) =>
  command;

用法:

getCommand({
  label: "execute-task",
  arguments: {
    task_name: {
      type: "string",
    },
  },
  handler(options) {
    // HandlerOptions<{
    //  task_name: {
    //     type: "string";
    //  };
    // }>;
    options;
  },
});

为了使 options 的类型更可读,我们可以使用以下实用类型:

type Prettify<T> = T extends infer R ? { [K in keyof R]: R[K] } : never;

type HandlerOptions<T extends Record<string, Argument>> = Prettify<{
  [K in keyof T]: TypeMap[T[K]["type"]];
}>;

通过这个方式,前面示例中 options 的类型将是:

{  
  task_name: string;
}
英文:

Currently, there is no way to do that directly with the type you want. Instead, we should add a generic parameter to Command interface, which will represent the arguments:

interface Command&lt;
  T extends Record&lt;string, Argument&gt; = Record&lt;string, Argument&gt;
&gt; {
  label: string;
  arguments: T;
  handler: (options: HandlerOptions&lt;T&gt;) =&gt; void;
}

For the HandlerOptions we will need a map to convert string literal of the argument to the actual type:

type TypeMap = {
  string: string;
  number: number;
};

In HandlerOptions we are going to use mapped types to achieve the desired type:

type HandlerOptions&lt;T extends Record&lt;string, Argument&gt;&gt; = {
  [K in keyof T]: TypeMap[T[K][&quot;type&quot;]];
}

Example:

// type Example = {
//   argument1: string;
// }
type Example = HandlerOptions&lt;{ argument1: { type: &quot;string&quot; } }&gt;;

We will also need a some function that would accept a command and return it. The only purpose of this function will be to infer the type of the arguments:

const getCommand = &lt;T extends Record&lt;string, Argument&gt;&gt;(command: Command&lt;T&gt;) =&gt;
  command;

Usage:

getCommand({
  label: &quot;execute-task&quot;,
  arguments: {
    task_name: {
      type: &quot;string&quot;,
    },
  },
  handler(options) {
    // HandlerOptions&lt;{
    //  task_name: {
    //     type: &quot;string&quot;;
    //  };
    // }&gt;
    options;
  },
});

To make the type of the options more readable we may use the the following utility type:

type Prettify&lt;T&gt; = T extends infer R ? { [K in keyof R]: R[K] } : never;

type HandlerOptions&lt;T extends Record&lt;string, Argument&gt;&gt; = Prettify&lt;{
  [K in keyof T]: TypeMap[T[K][&quot;type&quot;]];
}&gt;;

With this the type of the option for the previous case will be:

{  
  task_name: string;
}

playground

huangapple
  • 本文由 发表于 2023年5月28日 20:11:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/76351424.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定