在TypeScript

时间:2018-11-22 23:43:54

标签: typescript types

我正在尝试为实用程序函数编写类型定义:

function getter<T>(field1, field2?, field3?) {
    if (field3 !== undefined) {
        return (obj: T) => obj[field1][field2][field3];
    } else if (field2 !== undefined) {
        return (obj: T) => obj[field1][field2];
    } else {
        return (obj: T) => obj[field1];
    }
}

在给定以下接口的情况下,这样:

interface Contact { phone: string; }
interface Author { name: string; contact: Contact; }
interface Book { id: number; author: Author; }

实例示例:

const lordOfTheRings: Book = { id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };

您可以使用getter<Book>('author', 'name')获得一个函数,当使用参数lordOfTheRings调用该函数时,该函数将返回JRR Tolkien

因此,上面的函数可以正常工作,但类型安全。我希望能够编写getter<Book>('id')并愉快地进行编译,但是如果我要编写getter<Book>('banana'),请给它一个类型错误,因为banana不是{{ 1}}。我只希望它能深入到三个层次。

我一直在研究这个问题,并努力编写一个适用于所有边缘情况的类型定义。

我能做的是这样的:

Book

我可以这样使用:

interface Getter<T> {
    <K extends keyof T>(field1: K): (obj: T) => T[K];
    <K extends keyof T, L extends keyof T[K]>(field1: K, field2: L): (obj: T) => T[K][L];
    <K extends keyof T, L extends keyof T[K], M extends keyof T[K][L]>(field1: K, field2: L, field3: M): (obj: T) => T[K][L][M];
}

const bookGetter: Getter<Book> = getter;

就编译器而言,这是一种完美的行为,但我希望能够将类型指定为getter的参数,而不必像这样定义getter的特定实例。

我尝试了此功能的签名:

bookGetter('id')              // No error
bookGetter('title')           // Error
bookGetter('author')(lordOfTheRings).name; // No error
bookGetter('author', 'name'); // No error
bookGetter('author', 'age');  // Error
bookGetter('author', 'contact')(lordOfTheRings).phone;  // No error
bookGetter('author', 'contact')(lordOfTheRings).email;  // Error
bookGetter('author', 'contact', 'phone');  // No error
bookGetter('author', 'contact', 'email');  // Error

最初看起来很有希望,但在规范上却失败了,

function getter<T,
                K extends keyof T = keyof T,
                L extends keyof T[K] = keyof T[K],
                M extends keyof T[K][L] = keyof T[K][L]>(field1: K, field2?: L, field3?: M) {

请不要担心此功能的实用性-在这一点上,它已经成为类型规范中的一项明智的练习!我们可以破解吗?

Click here to go to an example TS playground with these in

使用TypeScript 3.1.4。

1 个答案:

答案 0 :(得分:5)

定义getter的方式中的问题是Typescript不支持部分参数推断。因此,当您说getter<Book>时,将不会推断其余类型参数,它们只会使用其默认值。这意味着T[K]将是T所有属性类型的并集,并且可能没有任何公共密钥,因此,您得到的关于never的错误。

部分类型推断可能会在3.3中添加到Typescript中(按照此PR),但是即使那样,您也必须编写类似getter<Book, *>的方法,为每种类型添加*您要推断的参数。

现在可以使用的解决方案(并且IMO实际上读起来更好)是使用函数currying(即返回函数的函数)。使用这种方法,您将在第一个调用中指定目标类型,并在第二个调用中让推理处理其余部分:

function getter<T>() {
  function makeGetter<K extends keyof T>(field1: K): (obj: T) => T[K];
  function makeGetter<K extends keyof T, L extends keyof T[K]>(field1: K, field2: L): (obj: T) => T[K][L];
  function makeGetter<K extends keyof T, L extends keyof T[K], M extends keyof T[K][L]>(field1: K, field2: L, field3: M): (obj: T) => T[K][L][M];
  function makeGetter(field1: keyof any, field2?: keyof any, field3?: keyof any) {
    if (field2 !== undefined && field3 !== undefined) {
      return (obj: any) => obj[field1][field2][field3];
    } else if (field2 !== undefined) {
      return (obj: any) => obj[field1][field2];
    } else {
      return (obj: any) => obj[field1];
    }
  }

  return makeGetter;
}

// ---- Example Usage

interface Contact { phone: string; }
interface Author { name: string; contact: Contact; }
interface Book { id: number; author: Author; }

const bookGetter: Getter<Book> = getter;

const lordOfTheRings: Book =
  { id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };

getter<Book>()('id')(lordOfTheRings).name;
getter<Book>()('bones');
getter<Book>()('id');    // No error - correct
getter<Book>()('title'); // Error - correct
getter<Book>()('author')(lordOfTheRings).name; //ok 
getter<Book>()('author', 'name'); // ok
getter<Book>()('author', 'age'); //error
getter<Book>()('author', 'contact')(lordOfTheRings).phone; // ok
getter<Book>()('author', 'contact')(lordOfTheRings).email; //error
getter<Book>()('author', 'contact', 'phone'); // ok 
getter<Book>()('author', 'contact', 'email'); //error