类型保护 -- Typescript基础篇(16)

1,286 阅读4分钟

如果我们有一个变量a,它的类型是联合类型string | number,我们希望调用slice方法。但是slice方法只存在于string类型中。所以我们需要缩小a的类型范围到string才能使用slice方法,否则会报错。这就是类型保护。

typeof

使用typeof判断联合联合类型的变量:

function foo(a: string | number) {
  // 'slice' does not exist on type 'string | number'
  a.slice();
  if (typeof a === "string") {
    // 此时a的类型被推断为string,可以直接使用slice
    a.slice();
  }
}

instanceof

如果是类的联合类型,则可使用instanceof

class A {
  a = "a";
}

class B {
  b = "b";
}

function bar(ins: A | B) {
  if (ins instanceof A) {
    // 此时ins被推断为A
    ins.a;
  } else {
    // 此时ins被推断为B
    ins.b;
  }
}

in

in可以安全的检查一个对象是否具有某些属性:

interface First {
  x: string;
}

interface Second {
  y: number;
}

function baz(obj: First | Second) {
  if ("x" in obj) {
    // 此时obj被推断为First
    obj.x;
  } else {
    // 此时obj被推断为Second
    obj.y;
  }
}

我们定义了两个接口,但是我们在做类型保护时,使用的是"x" in obj,而不是使用obj instanceof First

instanceof后需要跟一个变量。而接口只是一个类型。接口类型只是用于编译时的类型约束,而编译后的代码中不会存在任何接口相关的代码。类则不同(是一个类型,也是一个变量),除了在编译时做类型约束,也会存在于编译后的代码中。

自定义保护类型

除了上述的方法,我们还可以创建自定义的保护类型函数,它的返回值形如:parameterName is Type:

// 还是使用in代码示例中的First和Second两个接口
function isFirst(p: First | Second): p is First {
  // 暂时将p断言为First类型,尝试访问x属性,判断它是否存在
  return (p as First).x !== undefined;
}

其实该函数的返回值本质就是一个布尔值,但是我们使用p is First来让这个布尔值具有类型保护的作用,实际使用时:

function doWhat(p: First | Second) {
  if (isFirst(p)) {
    // 此时p被推断为First
    p.x;
  } else {
    // 此时p被推断为Second
    p.y;
  }
}

在3.7版本中中还增加了assertion functions,也可以用来做类型保护:

function assert(condition: any, msg?: string): asserts condition {
  if (!condition) {
    throw msg;
  }
}

其中condition是一个条件判断,assert函数的返回值是asserts condition。用于在conditiontrue时,缩小类型范围:

function doSomething(p: any) {
  assert(typeof p === "string");
  // 此时的p为string
  p.slice;
}

使用condition的形式对复杂类型的判断还是无能为力:

function doSomething(p: First | Second) {
  assert((val as First).x !== undefined);
  // 无法正确缩小范围,此时p还是First | Second
  p;
}

assert函数形式还有一种形式可以对复杂类型进行判断:

function assertVal(val: any, msg?: string): asserts val is string {
  if (typeof val === "string") {
    throw msg;
  }
}

function doSomething(p: any) {
  assertVal(p);
  // 此时的p为string
  p.slice;
}

这种形式就和之前自定义保护类型的p is First十分相似,如果判断复杂类型:

function assertVal(val: any, msg?: string): asserts val is First {
  if ((val as First).x !== undefined) {
    throw msg;
  }
}

function doSomething(p:any) {
  assertVal(p);
  // 此时p能把正确推断为First类型
  p;
}

可选链

在3.7新添加了可选链,让我们能够更安全的深层获取对象的属性,使用?.操作符。

let obj: {
  bar?: {
    baz?: any;
  };
} = {};
let x = obj?.bar?.baz;

一旦某一层为undefined或者为null,则最终的结果为undefined,否则就可以取到实际的值。试想如果没有可选链,我们必须层层判断:

let x =
  obj === null || obj === undefined
    ? undefined
    : obj.bar === null || obj.bar === undefined
    ? undefined
    : obj.bar.baz;

实际上,ts对可选链的编译结果与之类似:

var _a;
var obj;
var x = (_a = obj === null || obj === void 0 ? void 0 : obj.bar) === null || _a === void 0 ? void 0 : _a.baz;

与可选链类似还有一种非空断言操作符!.,它表示开发者自己确定该变量不是undefined或者null,如:

function makeSure(a?: string) {
  polyfill();

  // 此时a被推断为 string | undefined
  a.slice;

  function polyfill() {
    if (a === undefined) {
      a = "default";
    }
  }
}

虽然我们提前使用了polyfill保证了a变量肯定是一个string类型的变量,但ts对这种深层嵌套的改变并没有识别到,所以它还是推断为string | undefined,此时我们就可以用!.:

  a!.slice; // 告诉ts,我确定a不是一个undefined,你别在说a可能是undefined了

需要注意的是:!.只是在编译时做了一个类型说明,但是并没有像可选链一样做类型保护,比如可选链的例子替换为!.let x = obj!.bar!.baz,最终编译的结果:

var x = obj.bar.baz;