是否可以在 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 声明它始终返回 stringstring 映射:

_.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 首先创建一个包含所有keyvalue 对的联合(因此对于您的示例,这将是 key: "a"; value: "x"; | key: "b"; value: "y"; )。然后,在映射类型中,我们映射联合中的所有value 类型,对于每个value,我们使用Extract 提取原始key。只要没有重复值,这将很好地工作(如果有重复值,我们将在值出现时获得键的联合)

【讨论】:

非常聪明!当存在重复时,联合似乎是合适的,因为无法保证排序。我唯一不喜欢的是InvertResult 总是出现在结果类型中(检查时,它显示为InvertResult&lt;a: 'x', b: 'y'&gt; 而不是 x: "a"; y: "b"; )。但是类型是正确的,并且在我尝试过的所有情况下都表现良好,这可能是不可避免的。 @danvk 将 &amp; 添加到InvertResult 将扩展工具提示中的类型 这样做有什么坏处吗? 如果有人关心的话,我发现的最简洁的方法是type Invert&lt;M extends Record&lt;keyof M, keyof any&gt;&gt; = [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&lt;T&gt;

Playground

【讨论】:

非常聪明!感谢分享@ford04! 致对此解决方案的非PropertyKey 值有问题的人:如果您在主对象中有非PropertyKey 值,例如null 或对象(例如const o = a: "x", c: null 其中null 不能成为对象的键)您只需在上面的两个实用程序函数中将Record&lt;PropertyKey, PropertyKey&gt; 更改为Record&lt;Property, any&gt;,并通过删除非PropertyKeys 来解决问题。请注意,KeyFromValue 仍然可以正常工作并返回密钥。 (例如KeyFromValue&lt;null, typeof o&gt; 返回"c"

以上是关于是否可以在 TypeScript 中精确键入 _.invert?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 TypeScript 3+ 中正确键入通用元组剩余参数?

在 TypeScript 中,我可以轻松地键入命名箭头函数,但是如何在基于函数关键字的函数中做同样的事情呢?

如何在 TypeScript 中键入 Redux 操作和 Redux reducer?

如何使用 TypeScript 正确键入检查允许部分子树的嵌套记录?

如何使用 Typescript 在不丢失任何道具的情况下键入样式组件?

在 TypeScript 中键入匿名对象的属性