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

时间:2018-11-27 16:14:26

标签: typescript tuples unions

我想从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']

相关

3 个答案:

答案 0 :(得分:4)

您提到的用例(带有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是具有额外Person属性的superpowers,而randomPersonPerson是谁知道什么附加属性。您根本不能说作用在Object.keys()上的Person会产生一个仅 个已知键的数组。这是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,因此这里的任何解决方案都将选择一些最大的并集大小(例如,六个组成部分)并处理达到该大小的并集。我上面略带circuit回的代码经过精心设计,因此您可以主要通过复制和粘贴来扩展此最大大小。

另一个警告:取决于编译器能够按顺序分析条件类型中的重载函数签名,并且取决于编译器能够在保留顺序的同时将并集转换为重载函数。这些行为都不一定能保证以相同的方式工作,因此,每当新版本的TypeScript出现时,您都需要进行检查。

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


最后,请不要执行我在此处显示的任何操作。好的,希望对您有所帮助。祝你好运!

答案 1 :(得分: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"]

答案 2 :(得分:0)

由于得到了我的反馈,并且如果您不希望倒车,我最多可以更新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....]