是否可以从 {a: string, b: date, c: number}
这样的接口(interface)生成像 [string, date, number]
这样的元组类型?
场景
我正在尝试将类型添加到函数中,您可以按顺序传递对象或对象属性的值。 (别@我,代码不是我写的。)
// This is valid
bookRepo.add({
title: 'WTF',
authors: ['Herb Caudill', 'Ryan Cavanaugh'],
date: new Date('2019-04-04'),
pages: 123,
})
// This is also valid
bookRepo.add([
'WTF', // title
['Herb Caudill', 'Ryan Cavanaugh'], // authors
new Date('2019-04-04'), // date
123, // pages
])
所以我想的是一种生成包含接口(interface)属性类型的元组的方法:
interface Book {
title: string
authors: string | string[]
date: Date
pages: number
}
type BookTypesTuple = TupleFromInterface<T>
// BookTypesTuple = [
// string,
// string | string[],
// Date,
// number
// ]
所以我可以做这样的事情:
class Repo<T> {
// ...
add(item: T): UUID
add(TupleFromInterface<T>): UUID
}
编辑 该类确实有一个定义字段规范顺序的数组属性。像这样:
const bookRepo = new Repo<Book>(['title', 'authors', 'date', 'pages'])
不过,我正在为通用 Repo 创建类型定义,而不是为特定实现编写类型定义。所以类型定义事先并不知道该列表将包含什么。
最佳答案
如果Repo
构造函数采用属性名称的元组,然后该元组类型需要编码为 Repo
的类型打字工作。像这样:
declare class Repo<T, K extends Array<keyof T>> { }
在这种情况下,K
是 T
的键数组,以及 add()
的签名可以用 T
构建和 K
,像这样:
type Lookup<T, K> = K extends keyof T ? T[K] : never;
type TupleFromInterface<T, K extends Array<keyof T>> = { [I in keyof K]: Lookup<T, K[I]> }
declare class Repo<T, K extends Array<keyof T>> {
add(item: T | TupleFromInterface<T, K>): UUID;
}
你可以验证 TupleFromInterface
随心所欲:
declare const bookRepo: Repo<Book, ["title", "authors", "date", "pages"]>;
bookRepo.add({ pages: 1, authors: "nobody", date: new Date(), title: "Pamphlet" }); // okay
bookRepo.add(["Pamplet", "nobody", new Date(), 1]); // okay
为了完整(并展示一些棘手的问题),我们应该展示构造函数的类型:
declare class Repo<T extends Record<K[number], any>, K extends Array<keyof T> | []> {
constructor(keyOrder: K & (keyof T extends K[number] ? K : Exclude<keyof T, K[number]>[]));
add(item: T | TupleFromInterface<T, K>): UUID;
}
那里发生了很多事情。一、T
被限制为 Record<K[number], any>
这样粗略的值为T
可以从 K
推断出来.然后,K
的约束通过与空元组的联合扩大 []
, 作为 hint编译器更喜欢 K
的元组类型而不仅仅是数组类型。然后,构造函数参数被键入为 K
的交集。用conditional type这确保了 K
使用 T
的所有键而不仅仅是其中的一些。并非所有这些都是必要的,但它有助于发现一些错误。
剩下的大问题是 Repo<T, K>
需要两个类型参数,并且您想手动指定 T
离开时K
从传递给构造函数的值中推断出来。不幸的是,TypeScript 仍然缺少 partial type parameter inference ,所以它会尝试推断两者 T
和 K
,或要求您手动指定 T
和 K
,否则我们必须变得聪明。
如果让编译器同时推断出 T
和 K
, 它推断出比 Book
更广泛的东西:
// whoops, T is inferred is {title: any, date: any, pages: any, authors: any}
const bookRepoOops = new Repo(["title", "authors", "date", "pages"]);
正如我所说,您不能只指定一个参数:
// error, need 2 type arguments
const bookRepoError = new Repo<Book>(["title", "authors", "date", "pages"]);
您可以同时指定两者,但这是多余的,因为您仍然必须指定参数值:
// okay, but tuple type has to be spelled out
const bookRepoManual = new Repo<Book, ["title", "authors", "date", "pages"]>(
["title", "authors", "date", "pages"]
);
避免这种情况的一种方法是使用 currying将构造函数拆分为两个函数;一通电话 T
, 另一个为 K
:
// make a curried helper function to manually specify T and then infer K
const RepoMakerCurried = <T>() =>
<K extends Array<keyof T> | []>(
k: K & (keyof T extends K[number] ? K : Exclude<keyof T, K[number]>[])
) => new Repo<T, K>(k);
const bookRepoCurried = RepoMakerCurried<Book>()(["title", "authors", "date", "pages"]);
等效地,您可以创建一个辅助函数,它接受类型为 T
的虚拟参数。这被完全忽略但用于推断 T
和 K
:
// make a helper function with a dummy parameter of type T so both T and K are inferred
const RepoMakerDummy =
<T, K extends Array<keyof T> | []>(
t: T, k: K & (keyof T extends K[number] ? K : Exclude<keyof T, K[number]>[])
) => new Repo<T, K>(k);
// null! as Book is null at runtime but Book at compile time
const bookRepoDummy = RepoMakerDummy(null! as Book, ["title", "authors", "date", "pages"]);
您可以使用最后三种解决方案中的任何一种 bookRepoManual
, bookRepoCurried
, bookRepoDummy
最不打扰你。或者你可以放弃拥有 Repo
跟踪 add()
的元组接受变体.
无论如何,希望对您有所帮助;祝你好运!
关于 typescript :从界面创建元组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55522477/