打字稿:从接口创建元组 [英] Typescript: Create tuple from interface

查看:29
本文介绍了打字稿:从接口创建元组的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

是否可以从像 {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() 的签名可以由 TK,像这样:

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 TK,或要求您手动指定 TK,或我们必须聪明.

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.

如果让编译器同时推断 TK,它会推断出比 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 类型的虚拟参数,该参数被完全忽略但用于推断 TK:

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"]);

您可以使用后三个解决方案bookRepoManualbookRepoCurriedbookRepoDummy 中最不打扰您的任何一个.或者您可以放弃让 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆