验证 属性 存在的 Typescript class 吸气剂

Typescript class getters that validate property presence

我很喜欢一种新的(对我而言)模式,即从 api 响应中创建数据模型 类,因为它们可以具有可重用的派生逻辑,而不必从 api.

我的问题是有多个吸气剂,我想在相互引用(或实际上在其他文件中使用)时断言 属性 的存在,但没有,所以我想知道我在做什么丢失或更好的解决方案是:

type TodoResponse = {
    userId: number;
    id: number;
    title: string;
    completed: boolean;
    authors?: string[];
}


class Todo implements TodoResponse {
  userId!: number;
  id!: number;
  title!: string;
  completed!: boolean;
  authors?: string[];

  constructor(todo: TodoResponse) {
    Object.assign(this, todo);
  }

  get hasAuthors(): boolean {
    return Boolean(this.authors?.length);
  }

  get firstAuthor(): string | void {
    if (this.hasAuthors) return this.authors[0]
  // errors with: Object is possibly 'undefined'
  }

  get firstAuthor(): string | void {
    if (this.authors?.length) return this.authors[0]
  // this works but it feels redundant to duplicate logic from other getters
  }
}

你想要的是检查 todo.hasAuthors 作为 类型保护 可以用于 narrow [=14] 类型=] 到已知包含已定义 authors 属性 的内容。不幸的是,TypeScript 目前没有办法实现这一点。


首先,classes 无法实现 discriminated union types; otherwise you could have Todo be assignable to {hasAuthors: true, authors: string[]} | {hasAuthors: false, authors?: undefined}. You could maybe use type assertionsTodo 外部 看起来像这样:

interface TodoWithAuthors extends TodoResponse {
  hasAuthors: true,
  authors: string[],
  firstAuthor: string
}
interface TodoWithoutAuthors extends TodoResponse {
  hasAuthors: false,
  authors?: undefined,
  firstAuthor: void
}
type Todo = TodoWithAuthors | TodoWithoutAuthors;
const Todo = class Todo implements TodoResponse {
   /* snip, your original impl goes here */
} as new (todo: TodoResponse) => Todo;

const todo = new Todo({
  id: 1, userId: 2, title: "",
  completed: false, authors: Math.random() > 0.99 ? undefined : ["a", "b"]
});
if (todo.hasAuthors) {
  // no compiler errors here
  console.log(todo.authors.join(", ")) // a, b
  console.log(todo.firstAuthor.toUpperCase()) // A
}

但是从内部,编译器看不出 this.hasAuthorsthis.authors 有任何影响。所以这不会以你想要的方式帮助你。


TypeScript 确实有 user-defined type guard functions and methods 的概念,你可以在其中调用 boolean-returning 函数或方法,它会在其中一个上充当类型保护输入参数(或方法的 this 上下文)。所以如果 hasAuthors 是一个方法而不是 getter,你可以这样做:

class Todo implements TodoResponse {
  userId!: number;
  id!: number;
  title!: string;
  completed!: boolean;
  authors?: string[];

  constructor(todo: TodoResponse) {
    Object.assign(this, todo);
  }

  hasAuthors(): this is { authors: string[], firstAuthor: string } {
    return Boolean(this.authors?.length);
  }

  get firstAuthor(): string | void {
    if (this.hasAuthors()) return this.authors[0] // okay
  }

}

通过将 hasAuthors() 的 return 类型注释为 this is { authors: string[], firstAuthor: string },我是说 obj.hasAuthors()true 结果会缩小 obj.hasAuthors() 的类型obj 到具有定义的 string[] 属性 的东西(以及定义的 firstAuthor 属性)。这在 firstAuthor() 的实现中起作用。它也适用于 class:

const todo = new Todo({
  id: 1, userId: 2, title: "",
  completed: false, authors: Math.random() > 0.99 ? undefined : ["a", "b"]
});
if (todo.hasAuthors()) {
  // no compiler errors here
  console.log(todo.authors.join(", ")) // a, b
  console.log(todo.firstAuthor.toUpperCase()) // A
}

所以,太好了。不幸的是,没有类似的 user-defined 类型保护 属性 访问器方法 功能。 microsoft/TypeScript#43368 有一个开放的功能请求,其状态当前为“等待更多反馈”。这意味着他们可能甚至不会考虑实施这样的功能,除非他们从社区听到一些引人注目的用例。如果你想看到这种情况发生,你可能会考虑解决这个问题,给它一个 ,并解释为什么这会比当前的选项更好。但即使你这样做了,也不知道什么时候甚至是否可以使用这样的功能。


所以现在你有点卡住了。要么使用 if (this.authors?.length) return this.authors[0] 形式的冗余类型保护,要么使用像 if (this.hasAuthors) return this.authors![0] 这样的类型断言,要么重构你想要的实现。

Playground link to code