在TypeScript中定义类型安全的通用对象访问器函数

Lau*_*ren 3 types typescript

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

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

这样,给定以下接口:

interface Contact { phone: string; }
interface Author { name: string; contact: Contact; }
interface Book { id: number; author: Author; }
Run Code Online (Sandbox Code Playgroud)

和示例实例:

const lordOfTheRings: Book = { id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };
Run Code Online (Sandbox Code Playgroud)

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

因此,上面的函数可以正常工作,但类型安全。我希望能够编写getter<Book>('id')并愉快地进行编译,但是如果我要编写getter<Book>('banana'),则会给我一个类型错误,因为banana它不是的有效键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;
Run Code Online (Sandbox Code Playgroud)

我可以这样使用:

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

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

我尝试了此签名的功能:

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

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

getter<Book>('id');    // No error - correct
getter<Book>('title'); // Error - correct
getter<Book>('author')(lordOfTheRings).name; // Error - NOT correct (property name does not exist on number | Author)
getter<Book>('author', 'name'); // All the rest also error in the same way as this one (argument of type not assignable to parameter of type never)
Run Code Online (Sandbox Code Playgroud)

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

单击此处转到TS操场的示例

使用TypeScript 3.1.4。

Tit*_*mir 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
Run Code Online (Sandbox Code Playgroud)