英文:
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: 'string' | 'number'
}
interface Command {
label: string
arguments: Record<string, Argument>
}
// --
export default {
label: 'execute-task',
arguments: {
'task_name': {
type: 'string'
}
},
} as Command
Now consider that I would like to add a 'handler' function within this object;
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
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<...>
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<C extends Command> = {
[key in keyof C['arguments']]: ...
}
Attempted with ThisType<Command>
, '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: "string" | "number";
}
type HandlerOptions<Args extends Record<string, Argument>> = {
[key in keyof Args]: any;
};
interface Command<Label extends string, Args extends Record<string, Argument>> {
label: Label;
arguments: Args;
handler: (options: HandlerOptions<Args>) => void;
}
function createCommand<Label extends string, Args extends Record<string, Argument>>(
label: Label,
args: Args,
handler: (options: HandlerOptions<Args>) => void
): Command<Label, Args> {
return {
label,
arguments: args,
handler,
};
}
const testCommand = createCommand("execute-task", { task_name: { type: "string" } }, (options) => {
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: "string" | "number";
}
type HandlerOptions<Args extends Record<string, Argument>> = {
[key in keyof Args]: any;
};
interface Command<Label extends string, Args extends Record<string, Argument>> {
label: Label;
arguments: Args;
handler: (options: HandlerOptions<Args>) => void;
}
function createCommand<Label extends string, Args extends Record<string, Argument>>(
label: Label,
args: Args,
handler: (options: HandlerOptions<Args>) => void
): Command<Label, Args> {
return {
label,
arguments: args,
handler,
};
}
const testCommand = createCommand("execute-task", { task_name: { type: "string" } }, (options) => {
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<
T extends Record<string, Argument> = Record<string, Argument>
> {
label: string;
arguments: T;
handler: (options: HandlerOptions<T>) => 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<T extends Record<string, Argument>> = {
[K in keyof T]: TypeMap[T[K]["type"]];
}
Example:
// type Example = {
// argument1: string;
// }
type Example = HandlerOptions<{ argument1: { type: "string" } }>;
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 = <T extends Record<string, Argument>>(command: Command<T>) =>
command;
Usage:
getCommand({
label: "execute-task",
arguments: {
task_name: {
type: "string",
},
},
handler(options) {
// HandlerOptions<{
// task_name: {
// type: "string";
// };
// }>
options;
},
});
To make the type of the options
more readable we may use the the following utility type:
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"]];
}>;
With this the type of the option
for the previous case will be:
{
task_name: string;
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论