在打字稿中获取字典/对象键作为元组

Posted

技术标签:

【中文标题】在打字稿中获取字典/对象键作为元组【英文标题】:get dictionary/object keys as tuple in typescript 【发布时间】:2019-04-29 10:30:54 【问题描述】:

我想从 TS 3.1 中的对象中获取具有正确类型文字的正确元组类型:

interface Person 
  name: string,
  age: number


// $ExpectType ['name','age']
type ObjectKeysTuple = ToTuple<keyof Person>

为什么?

使用Object.keys(dictionary)时获取正确的字符串字面量元组

我无法找到解决方案,因为 keyof 扩展为 union 返回 ('name' | 'age')[],这绝对不是我们想要的。

type ObjectKeysTuple<T extends object> = [...Array<keyof T>]
type Test = ObjectKeysTuple<Person>
// no errors ????not good
const test: Test = ['age','name','name','name']

相关:

Convert an interface to a tuple in typescript Transform union type to intersection type

【问题讨论】:

联合可能是你在这里能做的最好的,除非你知道dictionary的键的顺序与你界面中声明的键的顺序相同,并且you don't really know that. 【参考方案1】:

您提到的用例为Object.keys() 提出了一个元组类型,充满了危险,我建议不要使用它。

第一个问题是 TypeScript 中的类型不是"exact"。也就是说,仅仅因为我有一个Person 类型的值,并不意味着该值包含 nameage 属性。想象一下:

interface Superhero extends Person 
   superpowers: string[]

const implausibleMan: Superhero =  
   name: "Implausible Man",
   age: 35,
   superpowers: ["invincibility", "shape shifting", "knows where your keys are"]

declare const randomPerson: Person;
const people: Person[] = [implausibleMan, randomPerson];
Object.keys(people[0]); // what's this?
Object.keys(people[1]); // what's this?

注意implausibleMan 是一个带有额外superpowers 属性的Person,而randomPerson 是一个带有谁知道额外属性的Person。您根本不能说作用于PersonObject.keys() 将生成一个 已知键的数组。这就是such feature requests不断收到rejected的主要原因。

第二个问题与键的顺序有关。即使您知道您正在处理包含所有并且仅接口中声明的属性的确切类型,您can't guarantee 将由Object.keys() 返回键,其顺序与界面。例如:

const personOne: Person =  name: "Nadia", age: 35 ;
const personTwo: Person =  age: 53, name: "Aidan" ;
Object.keys(personOne); // what's this?
Object.keys(personTwo); // what's this?

大多数合理的 JS 引擎会可能按照属性插入的顺序将属性交还给您,但您不能指望这一点。而且您当然不能指望插入顺序与 TypeScript 接口属性顺序相同。所以你很可能将["age", "name"] 当作["name", "age"] 类型的对象,这可能不太好。


所有这些我喜欢弄乱类型系统,所以我决定编写代码以类似于Matt McCutchen's answer to another question 的方式将联合转换为元组。它也充满危险,我建议不要这样做。以下注意事项。这里是:

// add an element to the end of a tuple
type Push<L extends any[], T> =
  ((r: any, ...x: L) => void) extends ((...x: infer L2) => void) ?
   [K in keyof L2]-?: K extends keyof L ? L[K] : T  : never

// convert a union to an intersection: X | Y | Z ==> X & Y & Z
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

// convert a union to an overloaded function X | Y ==> ((x: X)=>void) & ((y:Y)=>void)     
type UnionToOvlds<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;

// convert a union to a tuple X | Y => [X, Y]
// a union of too many elements will become an array instead
type UnionToTuple<U> = UTT0<U> extends infer T ? T extends any[] ?
  Exclude<U, T[number]> extends never ? T : U[] : never : never

// each type function below pulls the last element off the union and 
// pushes it onto the list it builds
type UTT0<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? Push<UTT1<Exclude<U, A>>, A> : []
type UTT1<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? Push<UTT2<Exclude<U, A>>, A> : []
type UTT2<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? Push<UTT3<Exclude<U, A>>, A> : []
type UTT3<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? Push<UTT4<Exclude<U, A>>, A> : []
type UTT4<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? Push<UTT5<Exclude<U, A>>, A> : []
type UTT5<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? Push<UTTX<Exclude<U, A>>, A> : []
type UTTX<U> = []; // bail out

让我们试试吧:

type Test = UnionToTuple<keyof Person>;  // ["name", "age"]

看起来它有效。

注意事项:您不能以编程方式为任意大小的联合执行此操作。 TypeScript 不允许您使用iterate over union types,因此这里的任何解决方案都将选择一些最大联合大小(例如,六个成分)并处理不超过该大小的联合。我上面有些迂回的代码是为了让你可以通过复制和粘贴来扩展这个最大尺寸。

另一个警告:它依赖于编译器能够按顺序分析条件类型中的重载函数签名,它依赖于编译器能够在保留顺序的同时将联合转换为重载函数。这些行为都不一定能保证以相同的方式工作,因此每次新版本的 TypeScript 发布时都需要检查这一点。

最后的警告:它没有经过太多测试,所以即使你保持 TypeScript 版本不变,它也可能充满各种 ? 有趣的陷阱。如果您对使用这样的代码很认真,那么在考虑在生产代码中使用它之前,您需要对其进行大量测试。


总之,不要做我在这里展示的任何事情。好的,希望有帮助。祝你好运!

【讨论】:

我知道所有的风险和订购问题“保证”。我的用例不仅更深入,而且不想在这里详细说明,因为它并不重要:) 问题是如何从联合中获取元组 :) 尽管有广泛的答案,但一些初学者会发现它非常有用。干杯【参考方案2】:

@jcalz 在阅读了您的实现之后非常令人印象深刻,我决定编写自己的“N”递归键版本,这将无限深入并保持联合 I.E “A”的顺序 | "B" 变成 ["A", "B"]

// add an element to the end of a tuple
type Push<L extends any[], T> =
  ((r: any, ...x: L) => void) extends ((...x: infer L2) => void) ?
     [K in keyof L2]-?: K extends keyof L ? L[K] : T  : never

export type Prepend<Tuple extends any[], Addend> = ((_: Addend, ..._1: Tuple) => any) extends ((
    ..._: infer Result
) => any)
    ? Result
    : never;
//
export type Reverse<Tuple extends any[], Prefix extends any[] = []> = 
    0: Prefix;
    1: ((..._: Tuple) => any) extends ((_: infer First, ..._1: infer Next) => any)
        ? Reverse<Next, Prepend<Prefix, First>>
        : never;
[Tuple extends [any, ...any[]] ? 1 : 0];



// convert a union to an intersection: X | Y | Z ==> X & Y & Z
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

// convert a union to an overloaded function X | Y ==> ((x: X)=>void) & ((y:Y)=>void)     
type UnionToOvlds<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;

// returns true if the type is a union otherwise false
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

// takes last from union
type PopUnion<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? A : never;

// takes random key from object
type PluckFirst<T extends object> = PopUnion<keyof T> extends infer SELF ? SELF extends keyof T ? T[SELF] : never;
type ObjectTuple<T, RES extends any[]> = IsUnion<keyof T> extends true ? 
    [K in keyof T]: ObjectTuple<Record<Exclude<keyof T, K>, never>, Push<RES, K>> extends any[]
        ? ObjectTuple<Record<Exclude<keyof T, K>, never>, Push<RES, K>>
        : PluckFirst<ObjectTuple<Record<Exclude<keyof T, K>, never>, Push<RES, K>>>
 : Push<RES, keyof T>;

/** END IMPLEMENTATION  */



type TupleOf<T extends string> = Reverse<PluckFirst<ObjectTuple<Record<T, never>, []>>>

interface Person 
    firstName: string;
    lastName: string;
    dob: Date;
    hasCats: false;

type Test = TupleOf<keyof Person> // ["firstName", "lastName", "dob", "hasCats"]

【讨论】:

哇! Reverse&lt;&gt; 疯了!我曾认为在打字稿中递归类型是不可能的。我怀疑在这里使用可以归纳操作任意长度元组的核心设备可能会弯曲以构建许多以前在该语言中无法访问的代数类型。我想知道它是否为缺少的可变参数类型功能提供了完整的解决方法?我现在看到它的工作方式是基于 infer 遵守延迟类型解析,而不是急切:raw.githubusercontent.com/unional/typescript-guidelines/master/… p.s.,不要在非常大的联合上使用 TupleOf&lt;&gt;,例如FontAwesome 中所有图标的名称——它会让你的编译器/linter/ide 的语言服务在你的 CPU 煮咖啡时挂起很长时间。 似乎分解了联合中的大约 7 或 8 个元素。可能各种深度的显式枚举更快/更实用。 :( 嗯我怀疑如果你牺牲顺序应该能够更深入,但事实上递归和重载之间的编译器工作没有区别(我相信)它只是归结为这个解决方案有更多的循环。我想我可以写一个更高效的【参考方案3】:

由于我首先收到反馈并更新了多达 18 个工会成员,如果您不关心反转,则可以将第一个版本简化为...

并且可以处理对象,而不仅仅是对象键或任何其他类型。

享受吧。

/* helpers */
type Overwrite<T, S extends any> =  [P in keyof T]: S[P] ;
type TupleUnshift<T extends any[], X> = T extends any ? ((x: X, ...t: T) => void) extends (...t: infer R) => void ? R : never : never;
type TuplePush<T extends any[], X> = T extends any ? Overwrite<TupleUnshift<T, any>, T &  [x: string]: X > : never;
type UnionToIntersection<U> =(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type UnionToOvlds<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;
type PopUnion<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? A : never;
/* end helpers */
/* main work */
type UnionToTupleRecursively<T extends any[], U> = 
    1: T;
    0: PopUnion<U> extends infer SELF ? UnionToTupleRecursively<TuplePush<T, SELF>, Exclude<U, SELF>> : never;
[[U] extends [never] ? 1 : 0]
/* end main work */

type UnionToTuple<U> = UnionToTupleRecursively<[], U>;

type LongerUnion =  name: "shanon"  | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
    | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18

declare const TestType: UnionToTuple<LongerUnion> // [18, 17, 16, 15, 14....]

使用最新版本的打字稿编辑有一个更新的更现代的版本,这个新版本依赖于未来极不可能破坏的行为;旧的实现依赖于可能会改变的行为

type UnionToIntersection<U> = (
  U extends never ? never : (arg: U) => never
) extends (arg: infer I) => void
  ? I
  : never;

type UnionToTuple<T> = UnionToIntersection<
  T extends never ? never : (t: T) => T
> extends (_: never) => infer W
  ? [...UnionToTuple<Exclude<T, W>>, W]
  : [];


  type test = UnionToTuple<"1" | 10 | name: "shanon">
  // ["1", 10, name: "shanon"]

【讨论】:

它打破了版本吗?它只显示const TestType: [x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never, x: never] Hi kung iv'e 添加了一个带有示例和更新更简单语法的编辑 感谢您的快速编辑,很高兴从这些非平凡的解决方案中学习函数式编程。

以上是关于在打字稿中获取字典/对象键作为元组的主要内容,如果未能解决你的问题,请参考以下文章

在打字稿中使用枚举作为接口键

如何从打字稿中的数组中获取键[重复]

避免任何类型在打字稿中获取枚举值

如何从打字稿中的json响应中获取日期对象

无法从打字稿中的 json 对象获取数组响应

如何使用打字稿中的查找来推断类型化的 mapValues?