在TypeScript中缩小类型的6种方法

818 阅读4分钟

Type Narrowing

在TypeScript程序中,一个变量可以从一个不太精确的类型转移到一个更精确的类型。这个过程被称为类型缩小。我们可以使用类型缩小来避免像下面这样的类型错误:

function addLeg(animal: Animal) {
  animal.legs = animal.legs + 1; // 💥 - Object is possibly 'undefined'
}

在这篇文章中,我们将介绍在TypeScript中缩小类型的6种不同方法。

使用条件值检查

上述例子中的Animal 类型如下:

type Animal = {
  name: string;
  legs?: number;
};

TypeScript引发了一个类型错误,因为addLeg 函数中的legs 属性,因为它可能是undefined ,而且将1 添加到undefined 是没有意义的。

一个解决方案是在增量之前检查legs 是否为真值:

function addLeg(animal: Animal) {
  if (animal.legs) {    animal.legs = animal.legs + 1;
  }}

legs 在 语句之前是类型 ,在 语句中被缩小为 。这种类型的缩小解决了类型错误的问题。if number | undefined if number

这种方法对于从类型中移除nullundefined 或从联合类型中移除字面意义是很有用的。

使用一个typeof 类型保护

考虑下面这个函数,如果参数是string ,它就会重复,如果是number ,它就会加倍:

function double(item: string | number) {
  if (typeof item === "string") {
    return item.concat(item); // item is of type string
  } else {
    return item + item; // item is of type number
  }
}

if 语句之前,item 参数的类型是numberstring 。在if 分支内,item 被缩小为string ,在else 分支内,item 被缩小为number

这种模式被称为typeof 类型保护,对于缩小原始类型的联合类型很有用。

使用instanceof 类型保护

我们使用的很多类型都比原始类型要复杂,考虑一下下面的类型:

class Person {
  constructor(
    public firstName: string,
    public surname: string
  ) {}
}
class Organisation {
  constructor(public name: string) {}
}
type Contact = Person | Organisation;

下面的函数引发了一个类型错误:

function sayHello(contact: Contact) {
  console.log("Hello " + contact.firstName);
  // 💥 - Property 'firstName' does not exist on type 'Contact'.
}

这是因为contact 可能属于Organisation 类型,它没有firstName 属性:

一个instanceof 的类型保护可以与类类型一起使用,如下所示:

function sayHello(contact: Contact) {
  if (contact instanceof Person) {    console.log("Hello " + contact.firstName);
  }}

contact 的类型在if 语句中被缩小为Person ,这就解决了类型错误。

使用一个in 类型保护

不过我们并不总是使用类来表示类型,上一个例子中的类型可以是这样的:

interface Person {
  firstName: string;
  surname: string;
}
interface Organisation {
  name: string;
}
type Contact = Person | Organisation;

instanceof 类型保护对接口或类型别名不起作用。相反,我们可以使用in 操作符类型保护:

function sayHello(contact: Contact) {
  if ("firstName" in contact) {    console.log("Hello " + contact.firstName);
  }}

if 语句中,contact 的类型被缩小为Person ,这意味着不会发生类型错误。

使用带有类型谓词的类型保护函数

考虑一下下面的例子:

type Rating = 1 | 2 | 3 | 4 | 5;

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();
  const rating = product.rating;
  return rating;
}

该函数的返回类型被推断为Promise<any> 。因此,我们如果给调用这个函数的结果分配一个变量,它将具有any 的类型:

const rating = await getRating("1"); // type of rating is `any`

这意味着在这个变量上不会发生类型检查。😞

我们可以使用一个具有类型谓词的类型保护函数来使这段代码更加类型安全。下面是类型保护函数:

function isValidRating(
  rating: any
): rating is Rating {
  if (!rating || typeof rating !== "number") {
    return false;
  }
  return (
    rating === 1 ||
    rating === 2 ||
    rating === 3 ||
    rating === 4 ||
    rating === 5
  );
}

rating is Rating 是上述函数中的类型谓词。

如果使用了类型谓词,类型保护函数必须返回一个布尔值。

我们可以在我们的示例代码中使用这个类型保护函数,如下所示:

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();  const rating = product.rating;
  if (isValidRating(rating)) {
    return rating; // type of rating is `Rating`
  } else {
    return undefined;
  }
}

if 语句里面的rating 的类型现在是Rating

不错!😀

使用带有断言签名的类型保护函数

继续看评级的例子,我们可能想用一点不同的方式来组织我们的代码:

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();  const rating = product.rating;
  checkValidRating(rating); // should throw error if invalid rating
  return rating; // type should be narrowed to `Rating`
}

在这里,我们希望checkValidRating 函数在评级无效的情况下抛出一个错误。

我们还希望rating 的类型在checkValidRating 成功执行后被缩小到Rating

我们可以使用一个带有断言签名的类型保护函数来实现这一点:

function checkValidRating(
  rating: any
): asserts rating is Rating {
  if (!rating || typeof rating !== "number") {
    throw new Error("Not a rating");
  }
  if (
    rating !== 1 &&
    rating !== 2 &&
    rating !== 3 &&
    rating !== 4 &&
    rating !== 5
  ) {
    throw new Error("Not a rating");
  }
}

asserts rating is Rating 是上述类型保护函数中的断言签名。如果该函数返回时没有出现错误,那么 参数就被断言为 类型。rating Rating

getRating 中,在调用checkValidRating 后,rating 变量的类型是Rating 。 😀

包裹起来

TypeScript在条件分支中自动缩小了变量的类型。做一个真实的条件检查将从一个类型中删除nullundefined 。一个typeof 类型保护是缩小原始类型联盟的一个好方法。instanceof 类型防护对于缩小类的类型很有用。in 类型防护是缩小对象类型的一个很好的方法。函数类型防护在更复杂的情况下很有帮助,因为在这种情况下需要检查变量的值以确定其类型。