是否可以在 TypeScript 中精确键入 _.invert?
Posted
技术标签:
【中文标题】是否可以在 TypeScript 中精确键入 _.invert?【英文标题】:Is it possible to precisely type _.invert in TypeScript? 【发布时间】:2019-10-18 07:52:00 【问题描述】:在 lodash 中,_.invert
函数反转对象的键和值:
var object = 'a': 'x', 'b': 'y', 'c': 'z' ;
_.invert(object);
// => 'x': 'a', 'y': 'b', 'z': 'c'
lodash 类型 currently 声明它始终返回 string
→string
映射:
_.invert(object); // type is _.Dictionary<string>
但有时,特别是如果您使用 const assertion,更精确的类型会更合适:
const o =
a: 'x',
b: 'y',
as const; // type is readonly a: "x"; readonly b: "y";
_.invert(o); // type is _.Dictionary<string>
// but would ideally be readonly x: "a", readonly y: "b"
是否有可能获得如此精确的打字?该声明即将结束:
declare function invert<
K extends string | number | symbol,
V extends string | number | symbol,
>(obj: Record<K, V>): [k in V]: K;
invert(o); // type is x: "a" | "b"; y: "a" | "b";
键是正确的,但值是输入键的并集,即您失去了映射的特异性。有没有可能做到完美?
【问题讨论】:
【参考方案1】:编辑
由于在映射类型中引入了as
子句,您可以将此类型写为:
You can use a mapped type with an as clause:
type InvertResult<T extends Record<PropertyKey, PropertyKey>> =
[P in keyof T as T[P]]: P
Playground Link
原答案
您可以使用保留正确值的更复杂的映射类型来做到这一点:
const o =
a: 'x',
b: 'y',
as const;
type AllValues<T extends Record<PropertyKey, PropertyKey>> =
[P in keyof T]: key: P, value: T[P]
[keyof T]
type InvertResult<T extends Record<PropertyKey, PropertyKey>> =
[P in AllValues<T>['value']]: Extract<AllValues<T>, value: P >['key']
declare function invert<
T extends Record<PropertyKey, PropertyKey>
>(obj: T): InvertResult<T>;
let s = invert(o); // type is x: "a"; y: "b";
Playground Link
AllValues
首先创建一个包含所有key
、value
对的联合(因此对于您的示例,这将是 key: "a"; value: "x"; | key: "b"; value: "y";
)。然后,在映射类型中,我们映射联合中的所有value
类型,对于每个value
,我们使用Extract
提取原始key
。只要没有重复值,这将很好地工作(如果有重复值,我们将在值出现时获得键的联合)
【讨论】:
非常聪明!当存在重复时,联合似乎是合适的,因为无法保证排序。我唯一不喜欢的是InvertResult
总是出现在结果类型中(检查时,它显示为InvertResult<a: 'x', b: 'y'>
而不是 x: "a"; y: "b";
)。但是类型是正确的,并且在我尝试过的所有情况下都表现良好,这可能是不可避免的。
@danvk 将 &
添加到InvertResult
将扩展工具提示中的类型
这样做有什么坏处吗?
如果有人关心的话,我发现的最简洁的方法是type Invert<M extends Record<keyof M, keyof any>> = [K in M[keyof M]]: [P in keyof M]: M[P] extends K ? P : never [keyof M] ;
如何将它与 _.invert() 一起使用以获得正确的类型?【参考方案2】:
随着 TypeScript 4.1 对Key Remapping in Mapped Types 的支持,这变得相当简单:
const o =
a: 'x',
b: 'y',
as const;
declare function invert<
T extends Record<PropertyKey, PropertyKey>
>(obj: T):
[K in keyof T as T[K]]: K
;
let s = invert(o); // type is readonly x: "a"; readonly y: "b";
playground
【讨论】:
declare 只定义了一个在类型检查后被删除的类型,因此该函数在运行时不存在,并且使操场示例甚至无法正常运行... 这里有趣的是类型。运行时实现是微不足道的。你可以自己写或者打电话给lodash的_.invert
。
简洁优雅。请注意as const
,否则您可能会浪费更多的时间而不是您愿意承认的。
虽然这可行,但它似乎混淆了 TS 服务器类型提示 - 如果你写 sx 类型提示将是:(property) a: "a"
- 也许as T[K
在这里有问题,因为 TS 现在转换提示输出的“x”到“a”?无论哪种方式,Titian Cernicova-Dragomir 的回答都比较冗长,但不会导致类型提示出现问题。
@notepadNinja "a"
是 s.x
的类型,但令人惊讶的是它显示的是 a: "a"
而不是 x: "a"
。也许提交一个 TS 错误?【参考方案3】:
Titian Cernicova-Dragomir 的解决方案非常酷。今天我找到了另一种使用条件类型交换对象键和值的方法:
type KeyFromValue<V, T extends Record<PropertyKey, PropertyKey>> =
[K in keyof T]: V extends T[K] ? K : never
[keyof T];
type Invert<T extends Record<PropertyKey, PropertyKey>> =
[V in T[keyof T]]: KeyFromValue<V, T>
;
用const o
测试:
const o =
a: "x",
b: "y"
as const;
// type Invert_o = x: "a"; y: "b";
type Invert_o = Invert<typeof o>;
// works
const t: Invert<typeof o> = x: "a", y: "b" ;
// Error: Type '"a1"' is not assignable to type '"a"'.
const t1: Invert<typeof o> = x: "a1", y: "b" ;
声明invert
函数的方式与上述答案相同,返回类型为Invert<T>
。
Playground
【讨论】:
非常聪明!感谢分享@ford04! 致对此解决方案的非PropertyKey 值有问题的人:如果您在主对象中有非PropertyKey
值,例如null
或对象(例如const o = a: "x", c: null
其中null
不能成为对象的键)您只需在上面的两个实用程序函数中将Record<PropertyKey, PropertyKey>
更改为Record<Property, any>
,并通过删除非PropertyKeys 来解决问题。请注意,KeyFromValue
仍然可以正常工作并返回密钥。 (例如KeyFromValue<null, typeof o>
返回"c"
)以上是关于是否可以在 TypeScript 中精确键入 _.invert?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 TypeScript 3+ 中正确键入通用元组剩余参数?
在 TypeScript 中,我可以轻松地键入命名箭头函数,但是如何在基于函数关键字的函数中做同样的事情呢?
如何在 TypeScript 中键入 Redux 操作和 Redux reducer?
如何使用 TypeScript 正确键入检查允许部分子树的嵌套记录?