打字稿:从接口创建元组

时间:2019-04-04 18:10:40

标签: typescript tuples

是否可以从类似[string, date, number]的接口生成类似{a: string, b: date, c: 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 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创作类型定义,而不是为特定的实现创作。因此,类型定义事先不知道该列表将包含什么。

2 个答案:

答案 0 :(得分:4)

可以在TS中编写类似util的类型。但是,对于您的用例来说,这是不可能的。

order 在类对象接口中无关紧要,而在类数组接口中则无关紧要。输入中不存在 order 信息,因此无法无处获得这种输出。


编辑1:响应OP的编辑:由于提供了所有必要的信息,因此乍看起来似乎存在解决方案。但是,由于TypeScript的类型定义语言的限制,我无法找到一种实现您需要的util类型TupleFromInterface的方法。到目前为止,我可以获得的最佳结果是:

type result = TupleFromInterface<Book, ['title', 'authors', 'date', 'pages']>
// yields:
type result = {
  0: string;
  1: string | string[];
  2: Date;
  3: number;
}

我找不到将这种result转换为所需元组的通用方法。如此接近成功!如果有人知道如何解决这个难题,请告诉我!


编辑2:响应@jcalz答案

这是我的方法,可以产生看起来很滑稽的误导元组显示。

type ArrayKeys = keyof any[]
type Indices<T> = Exclude<keyof T, ArrayKeys>
type Lookup<T, K> = K extends keyof T ? T[K] : never;
type TupleFromInterface<T, K extends Array<keyof T>> = {
  [I in Indices<K>]: Lookup<T, K[I]>
}

区别是我使用[I in Indices<K>]而不是[I in keyof K]。在change introduced in TS v3.1之前,keyof Array<any>还包含诸如"length" | "indexOf"之类的东西,这就是为什么我使用Indices来过滤掉它们的原因。

事实证明,这种方法不仅在v3.1 +中是不必要的,而且是不完善的。

type TupleLike = { 0: number };
let foo: TupleLike;
foo = [1] // good
foo = [1, 'string'] // <- also accepted, not ideal
foo = ['string'] // bad

结论,我的方法是一种旧的解决方法,当使用TS v3.1 +时,请参阅@jcalz的answer

答案 1 :(得分:3)

如果Repo构造函数采用属性名称的元组,则该元组类型需要以Repo的类型进行编码,以便键入工作。像这样:

declare class Repo<T, K extends Array<keyof T>> { }

在这种情况下,KT的键数组,并且add()的签名可以由TK构成,像这样:

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的元组类型,而不仅仅是数组类型。然后,将构造函数参数键入为Kconditional type的交集,以确保K使用T的键的 all 而不仅仅是其中一些。并非所有这些都是必要的,但是它有助于捕获一些错误。

剩下的最大问题是Repo<T, K>需要两个类型参数,并且您希望手动指定T,同时让K从传递给构造函数的值中推断出来。不幸的是,TypeScript仍然缺少partial type parameter inference,因此它将尝试推断两者 TK,或者要求您手动指定TK,否则我们必须变得聪明。

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

// 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()的元组接受变体。

无论如何,希望能有所帮助;祝好运!