我们假设有一个对象定义:
type Obj = {
i: number
b: boolean
s: string
}
我想定义一个类型,它允许我传递属性名称和一个回调,该回调需要与属性具有完全相同类型的单个参数:
const a: FormatEntry = {
accessor: 'b',
formatter: (value: boolean) => value ? 'a' : 'b'
}
我尽最大努力
type FormatEntry<K extends keyof Obj> = {
accessor: K,
formatter: (value: Obj[K]) => string
}
但是它给了我一个错误“泛型类型‘FormatEntry’需要 1 个类型参数。(2314)”,我不明白 - 我希望 TS 自己推断泛型的参数
当然,我可以提供这个论点:
const a: FormatEntry<'b'> = {
accessor: 'b',
formatter: (value: boolean) => value ? 'a' : 'b'
}
但这是双重工作。我做错了什么?
不幸的是,TypeScript 不会像您的 FormatEntry
那样推断 generic
types的类型参数。它只会在您调用泛型functions时推断类型参数。正如 microsoft/TypeScript#32794 中所要求的,这本质上是一个缺失的功能。如果要实现的话,也许你可以写
// NOT VALID TYPESCRIPT, DO NOT TRY THIS
type FormatEntry<K extends keyof Obj = infer> = {
accessor: K,
formatter: (value: Obj[K]) => string
}
const s: FormatEntry = { accessor: 's', formatter: s => s.toUpperCase() }
// ^? const s: FormatEntry<"s">
其中
K
类型参数具有假设的 infer
类型的 default,因此为
FormatEntry
类型的变量赋值将使 TypeScript 推断出参数,如上所示。不过,除非实现该功能,否则您需要解决它。
一种通用方法是使用通用辅助函数,其行为与您所需的类型类似:
const formatEntry = <K extends keyof Obj>(f: FormatEntry<K>) => f;
然后不要写
const s: FormatEntry = {⋯}
,而是写const s = formatEntry({⋯})
:
const s = formatEntry({ accessor: 's', formatter: s => s.toUpperCase() });
// ^? const s: FormatEntry<"s">
现在你得到了想要的推论。 TypeScript 从
K
的参数将 "s"
推断为 formatEntry()
,并且由于该函数仅返回其输入,因此 s
的类型为 FormatEntry<"s">
。
但对于所写的示例,您实际上可能根本不需要
FormatEntry
是通用的。从概念上讲,似乎“FormatEntry
”将是三个可能的通用版本的“并集”。也就是说,您想要一个相当于 FormatEntry<"i"> | FormatEntry<"b"> | FormatEntry<"s">
:的类型
type FormatEntry = {
accessor: "i";
formatter: (value: number) => string;
} | {
accessor: "b";
formatter: (value: boolean) => string;
} | {
accessor: "s";
formatter: (value: string) => string;
}
这是一个受歧视的联盟
,其中accessor
是
判别式属性:
let a: FormatEntry;
a = { accessor: 'b', formatter: value => value ? 'a' : 'b' };
a = { accessor: 's', formatter: s => s.toUpperCase() };
a = { accessor: 'i', formatter: i => i.toFixed(2) };
由于
a
是联合体类型,您可以将其重新分配给联合体的任何成员(而不是
推断永远只是一个成员),并且由于它是一个可区分的联合体,TS 将推断参数来自
formatter
属性值的 accessor
类型。上述类型的唯一问题是它是手动写出来的。我们可以通过将 TypeScript compute 设置为 Obj
所需的类型来解决这个问题,如下所示:
type FormatEntry = { [K in keyof Obj]: {
accessor: K,
formatter: (value: Obj[K]) => string
} }[keyof Obj];
这称为“分布式对象类型”,在microsoft/TypeScript#47109
中创造。我们的想法是,我们创建一个“映射类型”,然后立即使用完整的键集对其进行“索引”,从而形成一个并集。一般来说,如果你有一个泛型类型 F<P>
,其中 P
是一个类似键的类型,并且你想为某个联合 F<P>
中的每个 P
计算
K
的并集,那么你可以写 {[P in K]: F<P>}[K]
.因此,对于FormatEntry
中的每个FormatEntry<K>
,K
现在是一个可区分的联合,计算为分布式对象类型,基于keyof Obj
的原始定义,并且相当于上面手动编写的版本。
Playground 代码链接