typescript :从界面创建元组

标签 typescript tuples

是否可以从 {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>> { }

在这种情况下,KT 的键数组,以及 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 ,所以它会尝试推断两者 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"]);

您可以使用最后三种解决方案中的任何一种 bookRepoManual , bookRepoCurried , bookRepoDummy最不打扰你。或者你可以放弃拥有 Repo跟踪 add() 的元组接受变体.

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

关于 typescript :从界面创建元组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55522477/

相关文章:

visual-studio - 在 Visual Studio 中使用特定的 TypeScript 编译器版本

typescript - 从 OpenAPI 的 `oneOf` 关键字生成的不受欢迎的 Flow/TypeScript 类型。是否有另一个 OpenAPI 模式会产生更好的结果?

Angular:如何正确实现 APP_INITIALIZER

typescript - 如何使用 typescript 设置 axiosconfig?

python - 在一个元组内重复一个元组

c++ - c++0x 元组是否使用新的可变参数模板或 Boost 的宏元组实现?

javascript - 无法立即从 subscribe() 分配变量

c++ - 为什么 ADL 不能使用 std::get 解析为正确的函数

Python:删除 2 个 csv 文件中的重复元组

rust - 有什么方法可以递归地使元组变平?