打字稿:从接口创建元组 [英] Typescript: Create tuple from interface
问题描述
是否可以从像 {a: string, b: date, c: number}
这样的接口生成像 [string, date, number]
这样的元组类型?
Is it possible to generate a tuple type like [string, date, number]
from an interface like {a: string, b: date, c: number}
?
我正在尝试向函数添加类型,您可以在其中按顺序传递对象或对象属性的值.(别@我,代码不是我写的.)
I'm trying to add typings to a function where you can either pass an object, or the values of the object's properties, in order. (Don't @ me, I didn't write the code.)
// 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
])
所以我想象的是一种生成包含接口属性类型的元组的方法:
So what I'm imagining is a way to generate a tuple that contains an interface's properties' types:
interface Book {
title: string
authors: string | string[]
date: Date
pages: number
}
type BookTypesTuple = TupleFromInterface<T>
// BookTypesTuple = [
// string,
// string | string[],
// Date,
// number
// ]
所以我可以做这样的事情:
so I could do something like this:
class Repo<T> {
// ...
add(item: T): UUID
add(TupleFromInterface<T>): UUID
}
<小时>
编辑 该类确实具有定义字段规范顺序的数组属性.像这样:
Edit The class does have an array property that defines the canonical order of fields. Something like this:
const bookRepo = new Repo<Book>(['title', 'authors', 'date', 'pages'])
不过,我正在为通用 Repo 编写类型定义,而不是为特定实现编写类型定义.所以类型定义事先不知道该列表将包含什么.
I'm authoring type definitions for the generic Repo, though, not for a specific implementation. So the type definitions don't know in advance what that list will contain.
推荐答案
如果 Repo
构造函数采用属性名称的元组,则该元组类型需要编码为 的类型Repo
用于打字工作.像这样:
If the Repo
constructor takes a tuple of property names, then that tuple type needs to be encoded in the type of Repo
for the typing to work. Something like this:
declare class Repo<T, K extends Array<keyof T>> { }
在这种情况下,K
是一个 T
的键数组,add()
的签名可以由 T
和 K
,像这样:
In this case, K
is an array of keys of T
, and the signature for add()
can be built out of T
and K
, like this:
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
的行为是否符合您的要求:
And you can verify that TupleFromInterface
behaves as you want:
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
<小时>
为了完整(并展示一些棘手的问题),我们应该展示构造函数的类型:
To be complete (and show some hairy issues), we should show how the constructor would be typed:
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
,这样 T
的粗略值就可以从 推断出来>K
.然后,K
的约束通过与空元组 []
的联合来扩大,它用作 提示 让编译器更喜欢 K
的元组类型,而不仅仅是数组类型.然后,构造函数参数被输入为 K
与 条件类型 确保 K
使用 T
的所有键,而不仅仅是他们中的一些.并非所有这些都是必需的,但它有助于发现一些错误.
There's a lot going on there. First, T
is constrained to Record<K[number], any>
so that a rough value of T
can be inferred from just K
. Then, the constraint for K
is widened via a union with the empty tuple []
, which serves as a hint for the compiler to prefer tuple types for K
instead of just array types. Then, the constructor parameter is typed as an intersection of K
with a conditional type which makes sure that K
uses all of the keys of T
and not just some of them. Not all of that is necessary, but it helps catch some errors.
剩下的最大问题是 Repo
需要两个类型参数,您想手动指定 T
而离开 K
从传递给构造函数的值中推断出来.不幸的是,TypeScript 仍然缺乏部分类型参数推断,因此它要么尝试推断both T
和 K
,或要求您手动指定 T
和 K
,或我们必须聪明.
The big remaining issue is that Repo<T, K>
needs two type parameters, and you'd like to manually specify T
while leaving K
to be inferred from the value passed to the constructor. Unfortunately, TypeScript still lacks partial type parameter inference, so it will either try to infer both T
and K
, or require you to manually specify both T
and K
, or we have to be clever.
如果让编译器同时推断 T
和 K
,它会推断出比 Book
更宽的东西:
If you let the compiler infer both T
and K
, it infers something wider than Book
:
// whoops, T is inferred is {title: any, date: any, pages: any, authors: any}
const bookRepoOops = new Repo(["title", "authors", "date", "pages"]);
正如我所说,你不能只指定一个参数:
As I said, you can't specify just one parameter:
// error, need 2 type arguments
const bookRepoError = new Repo<Book>(["title", "authors", "date", "pages"]);
您可以同时指定两者,但这是多余的,因为您仍然必须指定参数值:
You can specify both, but that is redundant because you still have to specify the parameter value:
// 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
:
One way to circumvent this is to use currying to split the constructor into two functions; one call for T
, and the other for 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代码>:
Equivalently, you could make a helper function which accepts a dummy parameter of type T
that is completely ignored but is used to infer both T
and 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()
的元组接受变体.
You can use whichever of those last three solutions bookRepoManual
, bookRepoCurried
, bookRepoDummy
bothers you the least. Or you can give up on having Repo
track the tuple-accepting variant of add()
.
无论如何,希望有所帮助;祝你好运!
Anyway, hope that helps; good luck!
这篇关于打字稿:从接口创建元组的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!