打字稿:从接口创建元组

Her*_*ill 4 tuples typescript

是否可以[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
])
Run Code Online (Sandbox Code Playgroud)

所以我想象的是一种生成包含接口属性类型的元组的方法:

interface Book {
  title: string
  authors: string | string[]
  date: Date
  pages: number
}

type BookTypesTuple = TupleFromInterface<T>
// BookTypesTuple =  [
//   string,
//   string | string[],
//   Date,
//   number
// ]
Run Code Online (Sandbox Code Playgroud)

所以我可以做这样的事情:

class Repo<T> {
  // ...
  add(item: T): UUID
  add(TupleFromInterface<T>): UUID
}
Run Code Online (Sandbox Code Playgroud)

编辑该类确实有一个数组属性,用于定义字段的规范顺序。像这样的东西:

const bookRepo = new Repo<Book>(['title', 'authors', 'date', 'pages'])
Run Code Online (Sandbox Code Playgroud)

不过,我正在为通用 Repo 编写类型定义,而不是为特定实现编写类型定义。所以类型定义事先不知道该列表将包含什么。

jca*_*alz 6

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

declare class Repo<T, K extends Array<keyof T>> { }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,K是 的键数组T,并且add()可以用Tand构建签名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;
}
Run Code Online (Sandbox Code Playgroud)

您可以验证其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
Run Code Online (Sandbox Code Playgroud)

为了完整(并展示一些毛茸茸的问题),我们应该展示构造函数的类型:

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;
}
Run Code Online (Sandbox Code Playgroud)

那里有很多事情要做。首先,T被限制为 ,Record<K[number], any>以便T可以仅从 推断出的粗略值K。然后,约束对K经由联合加宽与空的元组[],其用作一个提示适合编译器执行偏爱元组类型K,而不是仅仅数组类型。然后,构造函数参数被键入为K条件类型的交集,以确保K使用所有键,T而不仅仅是其中一些键。并非所有这些都是必需的,但它有助于捕获一些错误。

剩下的一个大问题是Repo<T, K>需要两个类型参数,您希望手动指定,T同时K从传递给构造函数的值中推断出来。不幸的是,打字稿仍然缺少部分类型参数推断,所以它要么尝试推断 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"]);
Run Code Online (Sandbox Code Playgroud)

正如我所说,您不能只指定一个参数:

// error, need 2 type arguments
const bookRepoError = new Repo<Book>(["title", "authors", "date", "pages"]);
Run Code Online (Sandbox Code Playgroud)

可以同时指定两者,但这是多余的,因为您仍然必须指定参数值:

// okay, but tuple type has to be spelled out
const bookRepoManual = new Repo<Book, ["title", "authors", "date", "pages"]>(
  ["title", "authors", "date", "pages"]
);
Run Code Online (Sandbox Code Playgroud)

一种规避方法是使用柯里化将构造函数拆分为两个函数;一个调用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"]);
Run Code Online (Sandbox Code Playgroud)

等效地,您可以创建一个辅助函数,该函数接受一个类型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"]);
Run Code Online (Sandbox Code Playgroud)

您可以使用后三个解决方案中的任何一个bookRepoManualbookRepoCurried, 对bookRepoDummy您的困扰最少。或者您可以放弃Repo跟踪接受元组的add().

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