Typescript入门-类型断言讲解

对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。

ts 复制代码
type T = "a" | "b" | "c";
let foo = "a";

let bar: T = foo; // 报错

上面示例中,最后一行报错,原因是 TypeScript 推断变量 foo 的类型是 string,而变量 bar 的类型是 'a'|'b'|'c',前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。

TypeScript 提供了 "类型断言" 这样一种手段,允许开发者在代码中"断言"某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。

这种做法的实质是,允许开发者在某个位置"绕过"编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。

回到上面的例子,解决方法就是进行类型断言,在赋值时断言变量foo的类型。

ts 复制代码
type T = "a" | "b" | "c";

let foo = "a";
let bar: T = foo as T; // 正确

上面示例中,最后一行的 foo as T 表示告诉编译器,变量 foo 的类型断言为 T,所以这一行不再需要类型推断了,编译器直接把 foo 的类型当作 T,就不会报错了。

总之,类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。

类型断言有两种语法。

ts 复制代码
// 语法一:<类型>值
<Type>value;

// 语法二:值 as 类型
value as Type;

上面两种语法是等价的,value 表示值,Type 表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。

ts 复制代码
// 语法一
let bar: T = <T>foo;

// 语法二
let bar: T = foo as T;

上面示例是两种类型断言的语法,其中的语法一因为跟 JSX 语法冲突,使用时必须关闭 TypeScript 的 React 支持,否则会无法识别。由于这个原因,现在一般都使用语法二。

下面看一个例子。对象类型有严格字面量检查,如果存在额外的属性会报错。

ts 复制代码
// 报错
const p: { x: number } = { x: 0, y: 0 };

上面示例中,等号右侧是一个对象字面量,多出了属性y,导致报错。解决方法就是使用类型断言,可以用两种不同的断言。

ts 复制代码
// 正确
const p0: { x: number } = { x: 0, y: 0 } as { x: number };

// 正确
const p1: { x: number } = { x: 0, y: 0 } as { x: number; y: number };

上面示例中,两种类型断言都是正确的。第一种断言将类型改成与等号左边一致,第二种断言使得等号右边的类型是左边类型的子类型,子类型可以赋值给父类型,同时因为存在类型断言,就没有严格字面量检查了,所以不报错。

下面是一个网页编程的实际例子。

ts 复制代码
const username = document.getElementById("username");

if (username) {
  (username as HTMLInputElement).value; // 正确
}

上面示例中,变量 username 的类型是 HTMLElement | null,排除了 null 的情况以后,HTMLElement 类型是没有 value 属性的。如果 username 是一个输入框,那么就可以通过类型断言,将它的类型改成 HTMLInputElement,就可以读取value属性。

注意,上例的类型断言的圆括号是必需的,否则username会被断言成 HTMLInputElement.value,从而报错。

类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。

ts 复制代码
const data: object = {
  a: 1,
  b: 2,
  c: 3,
};

data.length; // 报错

(data as Array<string>).length; // 正确

上面示例中,变量data是一个对象,没有 length 属性。但是通过类型断言,可以将它的类型断言为数组,这样使用 length 属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译。

类型断言的一大用处是,指定 unknown 类型的变量的具体类型。

ts 复制代码
const value: unknown = "Hello World";

const s1: string = value; // 报错
const s2: string = value as string; // 正确

上面示例中,unknown 类型的变量 value 不能直接赋值给其他类型的变量,但是可以将它断言为其他类型,这样就可以赋值给别的变量了。

另外,类型断言也适合指定联合类型的值的具体类型。

ts 复制代码
const s1: number | string = "hello";
const s2: number = s1 as unknow as number;

上面示例中,变量 s1 是联合类型,可以断言其为联合类型里面的一种具体类型,再将其赋值给变量 s2。

类型断言的条件

类型断言并不意味着,可以把某个值断言为任意类型。

ts 复制代码
const n = 1;
const m: string = n as string; // 报错

上面示例中,变量 n 是数值,无法把它断言成字符串,TypeScript 会报错。

类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。

ts 复制代码
expr as T;

上面代码中,expr 是实际的值,T 是类型断言,它们必须满足下面的条件:exprT 的子类型,或者 Texpr 的子类型。

也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。

但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为 any 类型和 unknown 类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。

ts 复制代码
// 或者写成 <T><unknown>expr
expr as unknown as T;

上面代码中,expr 连续进行了两次类型断言,第一次断言为 unknown 类型,第二次断言为 T 类型。这样的话,expr 就可以断言成任意类型 T,而不报错。

下面是本小节开头那个例子的改写。

ts 复制代码
const n = 1;
const m: string = n as unknown as string; // 正确

上面示例中,通过两次类型断言,变量 n 的类型就从数值,变成了完全无关的字符串,从而赋值时不会报错。

as const 断言

如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。

ts 复制代码
// 类型推断为基本类型 string
let s1 = "JavaScript";

// 类型推断为字符串 "JavaScript"
const s2 = "JavaScript";

上面示例中,变量s1的类型被推断为 string,变量s2的类型推断为值类型 JavaScript。后者是前者的子类型,相当于 const 命令有更强的限定作用,可以缩小变量的类型范围。

有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。

ts 复制代码
let s = "JavaScript";

type Lang = "JavaScript" | "TypeScript" | "Python";

function setLang(language: Lang) {
  /* ... */
}

setLang(s); // 报错

上面示例中,最后一行报错,原因是函数 setLang() 的参数 language 类型是 Lang,这是一个联合类型。但是,传入的字符串 s 的类型被推断为 string,属于 Lang 的父类型。父类型不能替代子类型,导致报错。

一种解决方法就是把 let 命令改成 const 命令。

ts 复制代码
const s = "JavaScript";

这样的话,变量 s 的类型就是值类型 JavaScript,它是联合类型 Lang 的子类型,传入函数 setLang() 就不会报错。

另一种解决方法是使用类型断言。TypeScript 提供了一种特殊的类型断言 as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。

ts 复制代码
let s = "JavaScript" as const;
setLang(s); // 正确

上面示例中,变量 s 虽然是用 let 命令声明的,但是使用了 as const 断言以后,就等同于是用 const 命令声明的,变量 s 的类型会被推断为值类型 JavaScript。

使用了 as const 断言以后,let 变量就不能再改变值了。

ts 复制代码
let s = "JavaScript" as const;
s = "Python"; // 报错

上面示例中,let 命令声明的变量 s,使用 as const 断言以后,就不能改变值了,否则报错。

注意,as const 断言只能用于字面量,不能用于变量。

ts 复制代码
let s = "JavaScript";
setLang(s as const); // 报错

上面示例中,as const 断言用于变量 s,就报错了。下面的写法可以更清晰地看出这一点。

ts 复制代码
let s1 = "JavaScript";
let s2 = s1 as const; // 报错

另外,as const 也不能用于表达式。

ts 复制代码
let s = ("Java" + "Script") as const; // 报错

上面示例中,as const 用于表达式,导致报错。

as const 也可以写成前置的形式。

ts 复制代码
// 后置形式
expr as const

// 前置形式
<const>expr

as const 断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。

ts 复制代码
const v1 = {
  x: 1,
  y: 2,
}; // 类型是 { x: number; y: number; }

const v2 = {
  x: 1 as const,
  y: 2,
}; // 类型是 { x: 1; y: number; }

const v3 = {
  x: 1,
  y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }

上面示例中,第二种写法是对属性 x 缩小类型,第三种写法是对整个对象缩小类型。

总之,as const 会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。

下面是数组的例子。

ts 复制代码
// a1 的类型推断为 number[]
const a1 = [1, 2, 3];

// a2 的类型推断为 readonly [1, 2, 3]
const a2 = [1, 2, 3] as const;

上面示例中,数组字面量使用 as const 断言后,类型推断就变成了只读元组。

由于 as const 会将数组变成只读元组,所以很适合用于函数的 rest 参数。

ts 复制代码
function add(x: number, y: number) {
  return x + y;
}

const nums = [1, 2];
const total = add(...nums); // 报错

上面示例中,变量nums的类型推断为 number[],导致使用扩展运算符 ... 传入函数 add() 会报错,因为 add() 只能接受两个参数,而 ...nums 并不能保证参数的个数。

事实上,对于固定参数个数的函数,如果传入的参数包含扩展运算符,那么扩展运算符只能用于元组。只有当函数定义使用了 rest 参数,扩展运算符才能用于数组。

解决方法就是使用 as const 断言,将数组变成元组。

ts 复制代码
const nums = [1, 2] as const;
const total = add(...nums); // 正确

上面示例中,使用 as const 断言后,变量 nums 的类型会被推断为 readonly [1, 2],使用扩展运算符展开后,正好符合函数 add() 的参数类型。

Enum 成员也可以使用 as const 断言。

ts 复制代码
enum Foo {
  X,
  Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X

上面示例中,如果不使用 as const 断言,变量 e1 的类型被推断为整个 Enum 类型;使用了 as const 断言以后,变量 e2 的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。

非空断言

对于那些可能为空的变量(即可能等于 undefinednull),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号 !

ts 复制代码
function f(x?: number | null) {
  validateNumber(x); // 自定义函数,确保 x 是数值
  console.log(x!.toFixed());
}

function validateNumber(e?: number | null) {
  if (typeof e !== "number") throw new Error("Not a number");
}

上面示例中,函数 f() 的参数 x 的类型是 number|null,即可能为空。如果为空,就不存在x.toFixed()方法,这样写会报错。但是,开发者可以确认,经过 validateNumber() 的前置检验,变量 x 肯定不会为空,这时就可以使用非空断言,为函数体内部的变量x加上后缀!x!.toFixed()编译就不会报错了。

非空断言在实际编程中很有用,有时可以省去一些额外的判断。

ts 复制代码
const root = document.getElementById("root");

// 报错
root.addEventListener("click", (e) => {
  /* ... */
});

上面示例中,getElementById() 有可能返回空值 null,即变量 root 可能为空,这时对它调用 addEventListener() 方法就会报错,通不过编译。但是,开发者如果可以确认 root 元素肯定会在网页中存在,这时就可以使用非空断言。

ts 复制代码
const root = document.getElementById("root")!;

上面示例中,getElementById() 方法加上后缀 !,表示这个方法肯定返回非空结果。

不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。

ts 复制代码
const root = document.getElementById("root");

if (root === null) {
  throw new Error("Unable to find DOM element #root");
}

root.addEventListener("click", (e) => {
  /* ... */
});

上面示例中,如果root为空会抛错,比非空断言更保险一点。

非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。

ts 复制代码
class Point {
  x: number; // 报错
  y: number; // 报错

  constructor(x: number, y: number) {
    // ...
  }
}

上面示例中,属性 x 和 y 会报错,因为 TypeScript 认为它们没有初始化。

这时就可以使用非空断言,表示这两个属性肯定会有值,这样就不会报错了。

ts 复制代码
class Point {
  x!: number; // 正确
  y!: number; // 正确

  constructor(x: number, y: number) {
    // ...
  }
}

另外,非空断言只有在打开编译选项 strictNullChecks 时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为 undefinednull

断言函数

断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。

ts 复制代码
function isString(value) {
  if (typeof value !== "string") throw new Error("Not a string");
}

上面示例中,函数 isString() 就是一个断言函数,用来保证参数 value 是一个字符串。

下面是它的用法。

ts 复制代码
const aValue: string | number = "Hello";
isString(aValue);

上面示例中,变量 aValue 可能是字符串,也可能是数组。但是,通过调用 isString(),后面的代码就可以确定,变量 aValue 一定是字符串。

断言函数的类型可以写成下面这样。

ts 复制代码
function isString(value: unknown): void {
  if (typeof value !== "string") throw new Error("Not a string");
}

上面代码中,函数参数 value 的类型是 unknown,返回值类型是 void,即没有返回值。可以看到,单单从这样的类型声明,很难看出 isString() 是一个断言函数。

为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。

ts 复制代码
function isString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw new Error("Not a string");
}

上面示例中,函数 isString() 的返回值类型写成 asserts value is string,其中 assertsis 都是关键词,value 是函数的参数名,string 是函数参数的预期类型。它的意思是,该函数用来断言参数 value 的类型是 string,如果达不到要求,程序就会在这里中断。

使用了断言函数的新写法以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。

注意,函数返回值的断言写法,只是用来更清晰地表达函数意图,真正的检查是需要开发者自己部署的。而且,如果内部的检查与断言不一致,TypeScript 也不会报错。

ts 复制代码
function isString(value: unknown): asserts value is string {
  if (typeof value !== "number") throw new Error("Not a number");
}

上面示例中,函数的断言是参数 value 类型为字符串,但是实际上,内部检查的却是它是否为数值,如果不是就抛错。这段代码能够正常通过编译,表示 TypeScript 并不会检查断言与实际的类型检查是否一致。

另外,断言函数的 asserts 语句等同于 void 类型,所以如果返回除了 undefinednull 以外的值,都会报错。

ts 复制代码
function isString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw new Error("Not a string");
  return true; // 报错
}

上面示例中,断言函数返回了 true,导致报错。

下面是另一个例子。

ts 复制代码
type AccessLevel = "r" | "w" | "rw";

function allowsReadAccess(level: AccessLevel): asserts level is "r" | "rw" {
  if (!level.includes("r")) throw new Error("Read not allowed");
}

上面示例中,函数 allowsReadAccess() 用来断言参数 level 一定等于 r 或 rw。

如果要断言参数非空,可以使用工具类型 NonNullable<T>

ts 复制代码
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value === undefined || value === null) {
    throw new Error(`${value} is not defined`);
  }
}

上面示例中,工具类型 NonNullable<T> 对应类型 T 去除空类型后的剩余类型。

如果要将断言函数用于函数表达式,可以采用下面的写法。

ts 复制代码
// 写法一
const assertIsNumber = (value: unknown): asserts value is number => {
  if (typeof value !== "number") throw Error("Not a number");
};

// 写法二
type AssertIsNumber = (value: unknown) => asserts value is number;

const assertIsNumber: AssertIsNumber = (value) => {
  if (typeof value !== "number") throw Error("Not a number");
};

注意,断言函数与**类型保护函数(type guard)**是两种不同的函数。它们的区别是,断言函数不返回值,而类型保护函数总是返回一个布尔值。

ts 复制代码
function isString(value: unknown): value is string {
  return typeof value === "string";
}

上面示例就是一个类型保护函数 isString(),作用是检查参数 value 是否为字符串。如果是的,返回true,否则返回 false。该函数的返回值类型是 value is string,其中的 is 是一个类型运算符,如果左侧的值符合右侧的类型,则返回 true,否则返回 false。

如果要断言某个参数保证为真(即不等于falseundefinednull),TypeScript 提供了断言函数的一种简写形式。

ts 复制代码
function assert(x: unknown): asserts x {
  // ...
}

上面示例中,函数 assert() 的断言部分,asserts x 省略了谓语和宾语,表示参数 x 保证为真(true)。

同样的,参数为真的实际检查需要开发者自己实现。

ts 复制代码
function assert(x: unknown): asserts x {
  if (!x) {
    throw new Error(`${x} should be a truthy value.`);
  }
}

这种断言函数的简写形式,通常用来检查某个操作是否成功。

ts 复制代码
type Person = {
  name: string;
  email?: string;
};

function loadPerson(): Person | null {
  return null;
}

let person = loadPerson();

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) throw new Error(message);
}

// Error: Person is not defined
assert(person, "Person is not defined");
console.log(person.name);

上面示例中,只有 loadPerson() 返回结果为真(即操作成功),assert() 才不会报错。

相关推荐
码间舞3 小时前
你不知道的pnpm!如果我的电脑上安装了nvm,切换node版本后,那么pnpm还会共享一个磁盘的npm包吗?
前端·代码规范·前端工程化
用户1512905452203 小时前
itoa函数
前端
xiaominlaopodaren3 小时前
“UI里就可以请求数据库”,让你陌生的 React 特性
前端·javascript·react.js
薛定谔的算法3 小时前
JavaScript数组操作完全指南:从基础到高级
前端·javascript·算法
拜无忧3 小时前
前端,用SVG 模仿毛笔写字绘画,defs,filter
前端·css·svg
怪可爱的地球人3 小时前
ts的迭代器和生成器
前端
FlowGram3 小时前
Flowgram 物料库建设思考与实践
前端
用户22152044278003 小时前
Promise实例方法和async、await语法糖
前端·javascript
普通码农3 小时前
uniapp开发微信小程序使用takeSnapshot截图保存分享
前端