你提到的使用场景,为
Object.keys()
设计元组类型,存在很大的风险,我建议不要使用。
首先,TypeScript中的类型不是
"exact"的。也就是说,仅因为我有一个类型为
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]);
Object.keys(people[1]);
注意,
implausibleMan
是一个带有额外
superpowers
属性的
Person
,而
randomPerson
是一个带有不知道什么额外属性的
Person
。你无法说对一个
Person
使用
Object.keys()
将只产生已知键的数组。这就是为什么
此类功能请求一直被
拒绝的主要原因。
第二个问题与键的顺序有关。即使您知道正在处理包含接口中声明的所有且仅有的属性的确切类型,也
不能保证 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>
看起来它可以工作。
需要注意的是,您无法以编程方式处理任意大小的联合类型。TypeScript不允许您迭代联合类型,因此此处的任何解决方案都将选择一些最大联合大小(例如六个成分),并处理直至该大小的联合。上面有点迂回的代码是设计成您可以通过复制和粘贴来扩展这个最大值。
另一个需要注意的是:它取决于编译器能够按顺序分析条件类型中的重载函数签名,并且取决于编译器能够将联合转换为重载函数并保留顺序。这两种行为都不一定保证始终以相同的方式工作,因此每次新版本的TypeScript发布时都需要进行检查。
最后一个要注意的是:它尚未经过多少测试,因此即使在保持TypeScript版本不变的情况下,它可能充满各种有趣的陷阱。如果您真的想使用此类代码,则需要在考虑在生产代码中使用之前对其进行大量测试。
总之,不要按照我展示的做任何事情。好了,希望能帮到你。祝你好运!
dictionary
键的顺序与接口中声明的键的顺序相同,而且您并不真正知道。 - jcalz