英文:
Dynamically specify argument types based on data
问题
我有一个定义了一些变换函数的对象。这些函数具有相同数量的参数,但参数类型不同。我正在尝试找到一种方法,让 TypeScript 根据函数名称推断参数类型。
假设我们有一个包含一些函数的对象:
const funcList = {
f1: (val: string, opts: number) => { console.log(val, opts); },
f2: (val: number, opts: boolean) => { console.log(val, opts); },
} as const;
还有一个数据集需要我们迭代:
const data = [{
func: 'f1',
val: 'strval',
opt: 111,
}, {
func: 'f2',
val: 222,
opt: true,
}] as const;
现在,我想要遍历该数据数组并调用相应的函数:
for (const e of data) {
funcList[e.func](e.val, e.opt);
}
TypeScript 不允许此代码,并显示以下错误:
TS2345: 类型 'string | number' 的参数不能赋值给类型 'never' 的参数。 类型 'string' 不能赋值给类型 'never'。
似乎 TypeScript 不根据 func
字段来区分类型。但是,如果我将代码更改如下,一切都能正常工作:
for (const e of data) {
if (e.func === 'f1') {
funcList[e.func](e.val, e.opt);
} else {
funcList[e.func](e.val, e.opt);
}
}
因此,如果我明确检查元素的 func
字段,TypeScript 就可以正确推断其余部分的类型。
我可以做什么来避免使用大量的 if/switch 语句?
英文:
I have an object which defines some transform functions. Those functions have the same number of parameters but different parameter types. I'm trying to figure out a way to make TS to infer parameter types based on a function name.
Let's say we have an object with some functions:
const funcList = {
f1: (val: string, opts: number) => { console.log(val, opts); },
f2: (val: number, opts: boolean) => { console.log(val, opts); },
} as const;
And there is data set that we need to iterate through
const data = [{
func: 'f1',
val: 'strval',
opt: 111,
}, {
func: 'f2',
val: 222,
opt: true,
}] as const;
And now I want to iterate over that data array and call a corresponding function
for (const e of data) {
funcList[e.func](e.val, e.opt);
}
TS doesn't allow this code with the following error
> TS2345: Argument of type 'string | number' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.
It seems that TS doesn't discriminate type based on func
field.
However if I change the code as the following, everything works
for (const e of data) {
if (e.func === 'f1') {
funcList[e.func](e.val, e.opt);
} else {
funcList[e.func](e.val, e.opt);
}
}
So if I explicitly check element's func
field, TS can then infer the rest with proper types.
What could I do to avoid using tons of ifs/switch?
答案1
得分: 1
编译器无法处理 ms/TS#30581 中描述的“相关联的联合类型”,但是在 ms/TS#47109 中有一个建议的重构,考虑转向泛型。
首先,我们需要一个将所有与函数类型相关的内容都保存在其中的映射类型:
type RecordMap = {
f1: {
args: { val: string; opt: number };
return: number;
};
f2: {
args: { val: number };
return: void;
};
};
现在,让我们为 funcList
创建一个类型,我们将使用 映射类型。我们将遍历 RecordMap
的键,并从其每个元素中获取相关类型:
type FuncList = {
[K in keyof RecordMap]: (
args: RecordMap[K]['args'],
) => RecordMap[K]['return'];
};
结果:
type FuncList = {
f1: (args: {
val: string;
opt: number;
}) => number;
f2: (args: {
val: number;
}) => void;
}
让我们在 funcList
上使用 FuncList
:
const funcList: FuncList = {
f1: (args) => {
return args.opt;
},
f2: (args) => {
console.log(args);
},
};
我们不需要在 funcList
中显式包含类型,因为它们是从 FuncList
推断出来的。
接下来,我们需要为函数参数创建一个映射类型,它将返回传递的函数名称或如果未传递名称则返回可能的参数联合:
type FuncListArgs<T extends keyof RecordMap = keyof RecordMap> = {
[K in T]: {
func: K;
} & RecordMap[K]['args'];
}[T];
结果:
// {
// func: "f1";
// } & {
// val: string;
// opt: number;
// }
type F1Args = FuncListArgs<'f1'>
// ({
// func: "f1";
// } & {
// val: string;
// opt: number;
// }) | ({
// func: "f2";
// } & {
// val: number;
// })
type AllArgs = FuncListArgs
由于我们需要一个泛型参数来使整个过程工作,我们将创建一个接受某个函数的参数的函数:
const handleFunc = <T extends keyof RecordMap>(args: FuncListArgs<T>) => {
const func = funcList[args.func];
return func(args); // 无错误
};
用法:
for (const a of data) {
handleFunc(a);
}
英文:
The compiler is unable to handle the "correlated union types" described in ms/TS#30581, however, there is a suggested refactor described in ms/TS#47109, which considers moving to generics.
First, we will need some map type that will hold the everything related to types of functions:
type RecordMap = {
f1: {
args: { val: string; opt: number };
return: number;
};
f2: {
args: { val: number };
return: void;
};
};
Now, let's create a type for funcList
, which we will do using mapped types. We will map through RecordMap
keys and take relevant types from each of its element:
type FuncList = {
[K in keyof RecordMap]: (
args: RecordMap[K]['args'],
) => RecordMap[K]['return'];
};
Result:
type FuncList = {
f1: (args: {
val: string;
opt: number;
}) => number;
f2: (args: {
val: number;
}) => void;
}
Let's use FuncList
on the funcList
:
const funcList: FuncList = {
f1: (args) => {
return args.opt;
},
f2: (args) => {
console.log(args);
},
};
We don't need to include types in the funcList
explicitly, since they are inferred from FuncList
.
Next, we need to create a mapped type for the function arguments, which will return a union of possible arguments, for passed name of the function or for all of them if name is not passed:
type FuncListArgs<T extends keyof RecordMap = keyof RecordMap> = {
[K in T]: {
func: K;
} & RecordMap[K]['args'];
}[T];
Result:
// {
// func: "f1";
// } & {
// val: string;
// opt: number;
// }
type F1Args = FuncListArgs<'f1'>
// ({
// func: "f1";
// } & {
// val: string;
// opt: number;
// }) | ({
// func: "f2";
// } & {
// val: number;
// })
type AllArgs = FuncListArgs
Since we need a generic argument to make this whole thing to work, we will create a function that will accept an args for some function:
const handleFunc = <T extends keyof RecordMap>(args: FuncListArgs<T>) => {
const func = funcList[args.func];
return func(args); // no error
};
Usage:
for (const a of data) {
handleFunc(a);
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论