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

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

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

interface Person {
  name: string,
  age: number
}

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

为什么?:

使用

Object.keys(dictionary)

时获取正确的字符串文字元组

我无法找到解决方案,因为

keyof
扩大到并集,返回
('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']

相关:

typescript tuples unions
3个回答
23
投票

你提到的用例,为

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

第一个问题是 TypeScript 中的类型不是“精确”。 也就是说,仅仅因为我有一个类型为

Person
的值,并不意味着该值仅包含 name
age
属性。 想象一下以下情况:
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

是一个带有额外

Person
属性的
superpowers
,而
randomPerson
是带有谁知道有哪些额外属性的
Person
。 您根本不能说作用于
Object.keys()
Person
将生成一个仅包含已知键的数组。 这就是
此类功能请求
不断被拒绝的主要原因。 第二个问题与键的顺序有关。 即使您知道您正在处理包含所有且仅接口中声明的属性的确切类型,您也

不能保证

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 对另一个问题的回答

的方式将联合转换为元组。 它也充满危险,我建议不要这样做。 以下注意事项。在这里:

// 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 不允许您
迭代联合类型

,因此这里的任何解决方案都将选择一些最大联合大小(例如,六个组成部分)并处理最大到该大小的联合。 我上面有些迂回的代码是为了让您可以通过复制和粘贴来扩展这个最大大小。

另一个警告:它取决于编译器能够按顺序分析条件类型中的重载函数签名,并且取决于编译器能够在保留顺序的同时将联合转换为重载函数。 这些行为都不一定保证以相同的方式继续工作,因此每次新版本的 TypeScript 出现时,您都需要检查这一点。 最后的警告:它还没有经过太多测试,所以即使你保持 TypeScript 版本不变,它也可能充满各种有趣的陷阱。 如果您真的想使用这样的代码,那么在考虑在生产代码中使用它之前,您需要对其进行大量测试。

总之,不要做我在这里展示的任何事情。


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


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"}]

@jcalz

10
投票

// 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"]

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