为该属性的属性名称和值格式化程序对定义通用

问题描述 投票:0回答:1

我们假设有一个对象定义:

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 typescript-generics
1个回答
0
投票

不幸的是,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 代码链接
    

© www.soinside.com 2019 - 2024. All rights reserved.