如何让 Typescript 的某些属性成为可选的

8,946 阅读3分钟

Photo by Kevin Ku on Unsplash

接口可选属性的声明,你可以在属性名旁边加上一个问号 ? 来实现。

interface Person {
  uuid: string;
  name: string;
  surname: string;
  sex: string;
  height: number;
  age: number;
  pet?: Animal;
}

interface Animal {
  name: string;
  age: number;
}

上面例子中,pet 是一个可选属性。它很有用,每次你想对 Personpet 属性执行一些操作时,你需要检查它是否声明。 pet?: Animal; 写法就是 pet: Animal | undefined; 的简写。

小案例调研

我们假设,你想实现一个函数,该函数返回名字的首字母和姓,比如 J. Doe.

function personShortName(name: string, surname: string): string {
  return `${name[0]} ${surname}`;
}

const person1: Person = {
  /* Person properties */
};

personShortName(person1.name, person1.surname);

而不像上述代码,每次调用 personShortName() 去访问 namesurname ,我们可以使用 Person 类型的对象作为函数参数。

function personShortName(person: Person): string {
  return `${person.name[0]} ${person.surname}`;
}

当然,我们也可以使用解构来简化这个函数,如下所示:

function personShortName({ name, surname }: Person): string {
  return `${name[0]} ${surname}`;
}

看起来不错,但有限制。你需要一个 Person 类型的对象来调用一个简单的函数。现在,如果你想在其他地方调用它,你需要创建一个新的 Person

personShortName({ name: 'Robert', surname: 'Doe' });

上面的示例无法执行,因为 { name: string, surname: string }Person 类型不匹配。

那么如何实现双赢呢?

Typescript 有一个 Partial<T> 实用工具类型,这使得 T 的所有属性都是可选的。如果在 personShortName() 中使用 Partial<Person> 而不是 Person 作为参数类型,那么前面的示例就可执行了:

function personShortName({name, surname}: Partial<Person>): string {
    return `${name[0]} ${surname}`;
}

personShortName({ name: 'Robert', surname: 'Doe' });

但是我们这里有一个副作用,{ name: string; surname: string } 会变为 { name: string | undefined; surname: string | undefined; }

然后,为了防止一些奇怪的结果,比如 “u. undefined”,我们需要在函数内部执行一些类型检查,确保每个需要用到的变量都是被定义了。

function personShortName({ name, surname }: Partial<Person>): string | null {
  return name && surname ? `${name[0]} ${surname}` : null;
}

现在我们已经实现了另一个副作用,函数可能返回null,这可能强制在使用它的地方进行一些额外的检查。

但是别担心,这里有个更好的办法。TypeScript 提供了另一个实用工具类型—— Pick<T, Keys> ,从 T 中选择一些属性 Keys

function personShortName({
  name,
  surname,
}: Pick<Person, 'name' | 'surname'>): string {
  return `${name[0]} ${surname}`;
}

通过这种方式我们实现了我们的目标——我们允许通过两种方式调用这个函数,传递一个有效的 Person 对象或仅仅需要的变量作为参数。

personShortName({ name: 'John', surname: 'Doe' });

const someone: Person = { name: 'John', ... }      
personShortName(someone);

延伸学习

上述解决方案中,你无法访问 personShortName 函数中的其余 Person 属性。让我们假设你希望实现一个只在最简单变量中使用 namesurname ,但你也希望访问其他 Person 属性。

function personShortName(person: Person): string {
  const { name, surname, uuid } = person;
  if (uuid) {
    const userRole = getUserRole(uuid);
    return `${name[0]} ${surname}, ${userRole}`;
  }

  /* some other operations on persons params */

  return `${name[0]} ${surname}`;
}

这种实现不允许你直接使用带有 namesurname 的简单调用。但是,当没有 uuid 时,我们不需要所有的 Person 属性只是为了返回简短的名称。我们需要访问所有 Person 属性,但只需要 namesurname 。也许我们让 person 定义为 Person | Pick<Person, 'name' | 'surname'>

function personShortName(person: Person | Pick<Person, 'name' | 'surname'>)): string  {
  ❌ const { name, surname, uuid } = person;
  if (uuid) {
    const userRole = getUserRole(uuid);
    return `${name[0]} ${surname}, ${userRole}`;
  }

  /* some other operations on persons params */

  return `${name[0]} ${surname}`;
}

这是一个好主意,但是还是行不通。编译器将报错:属性uuid 在类型{ name: string, surname: string } 上不存在。

解决方案就是组合

这种方法是一个很好的方向,因为我们生命需要一个完整的 Person{ name, surname }。但事实上,我们不想要整个 Person{ name, surname }。我们想要完成的事情是让所有的 Person 属性都是可选的,并且 name, surname 是必需的。我们可以通过组合 Partial<T>Pick<T, Keys> 使用工具类型来实现这一点。

function personShortName(person: Partial<Person> & Pick<Person, 'name' | 'surname'>)): string  {
  const { name, surname, uuid } = person;
  if (uuid) {
    const userRole = getUserRole(uuid);
    userRole = getUserRole(uuid);
    return `${name[0]} ${surname}, ${userRole}`;
  }

  /* some other operations on persons params */

  return `${name[0]} ${surname}`;
}

上面的语法,Partial<Person> & Pick<Person, 'name' | 'surname'> 给了我们这样的类型:

person: {
    uuid?: string;
    name: string; <- required
    surname: string; <- required
    sex?: string;
    height?: number;
    age?: number;
    pet?: Animal;
}

通过这种方式,我们可以定义一些必需的属性,而其他属性则成为可选的。希望你会发现它很有用。