类型
在 JavaScript 中,原始类型指的是非对象且没有方法的数据类型,它包括 string、number、bigint、boolean、undefined 和 symbol
这六种 (null 是一个伪原始类型,它在 JavaScript 中实际上是一个对象,且所有的结构化类型都是通过 null 原型链派生而来)。
在 JavaScript 语言中,原始类型值是最底层的实现,对应到 TypeScript 中同样也是最底层的类型。
基础类型
- string
- number
- bigint
- boolean
- symbol
- Array
- object
let firstname: string = 'Captain'; // 字符串字面量
let familyname: string = String('S'); // 显式类型转换
let fullname: string = `my name is ${firstname}.${familyname}`; // 模板字符串
/** 十进制整数 */
let integer: number = 6;
/** 十进制整数 */
let integer2: number = Number(42);
/** 十进制浮点数 */
let decimal: number = 3.14;
/** 二进制整数 */
let binary: number = 0b1010;
/** 八进制整数 */
let octal: number = 0o744;
/** 十六进制整数 */
let hex: number = 0xf00d;
let big: bigint = 100n;
/** TypeScript 真香 为 真 */
let TypeScriptIsGreat: boolean = true;
/** TypeScript 太糟糕了 为 否 */
let TypeScriptIsBad: boolean = false;
let sym1: symbol = Symbol();
let sym2: symbol = Symbol("42");
/** 子元素是数字类型的数组 */
let arrayOfNumber: number[] = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arrayOfString: string[] = ["x", "y", "z"];
/** 子元素是数字类型的数组 */
let arrayOfNumber: Array<number> = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arrayOfString: Array<string> = ["x", "y", "z"];
// 如果我们明确指定了数组元素的类型,以下所有操作都将因为不符合类型约定而提示错误。
let arrayOfNumber: number[] = ["x", "y", "z"]; // 提示 ts(2322)
arrayOfNumber[3] = "a"; // 提示 ts(2322)
arrayOfNumber.push("b"); // 提示 ts(2345)
let arrayOfString: string[] = [1, 2, 3]; // 提示 ts(2322)
arrayOfString[3] = 1; // 提示 ts(2322)
arrayOfString.push(2); // 提示 ts(2345)
object 类型表示非原始类型的类型,即非 number、string、boolean、bigint、symbol、null、undefined 的类型。
declare function create(o: object | null): any;
create({}); // ok
create(() => null); // ok
create(2); // ts(2345)
create("string"); // ts(2345)
元组
元组可以限制数组元素的个数和类型,它特别适合用来实现多值返回。
import { useState } from 'react';
function useCount() {
const [count, setCount] = useState(0);
return ....;
}
// useState 的返回值类型是一个元组类型
(state: State) => [State, SetState]
元组相较对象而言,不仅为我们实现解构赋值提供了极大便利,还减少了不少代码量,这可能也是 React 官方如此设计核心 Hooks 的重要原因之一。
但事实上,许多第三方的 Hooks 往往会出于扩展性、稳定性等考虑,尤其是需要返回的值的个数超过 2 个时,会更偏向于使用对象作为返回值。
这里需要注意:数组类型的值只有显示添加了元组类型注解后(或者使用 as const,声明为只读元组),TypeScript 才会把它当作元组,否则推荐出来的类型就是普通的数组类型。
📢注意点
-
虽然
number
和bigint
都表示数字,但是这两个类型不兼容。 -
非严格模式下, string、 number、boolean 允许值为空(null)。严格模式下则不允许。
-
TypeScript 的数组和元组转译为 JavaScript 后都是数组。
-
不要将 TypeScript 中 Number、String、Boolean、Symbol 等类型和小写格式对应的 number、string、boolean、symbol 进行等价。实际上,我们压根使用不到 Number、String、Boolean、Symbol 类型,因为它们并没有什么特殊的用途。这就像我们不必使用 Number、String、Boolean 等构造函数 new 一个相应的实例一样。
let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)📢 不要使用对象类型来注解值的类型,因为这没有任何意义!
特殊类型
这是并不是 TypeScript 官方的定义
- any
- unknown
- void
- undefined
- never
any 指的是一个任意类型,它是官方提供的一个选择性绕过静态类型检测的作弊方式。
我们可以对被注解为 any 类型的变量进行任何操作,包括获取事实上并不存在的属性、方法,并且 TypeScript 还无法检测其属性是否存在、类型是否正确。
比如我们可以把任何类型的值赋值给 any 类型的变量,也可以把 any 类型的值赋值给任意类型(除 never 以外)的变量,如下代码所示:
let anything: any = {};
anything.doAnything(); // 不会提示错误
anything = 1; // 不会提示错误
anything = "x"; // 不会提示错误
let num: number = anything; // 不会提示错误
let str: string = anything; // 不会提示错误
如果我们不想花费过高的成本为复杂的数据添加类型注解,或者已经引入了缺少类型注解的第三方组件库,这时就可以把这些值全部注解为 any 类型,并告诉 TypeScript 选择性地忽略静态类型检测。
尤其是在将一个基于 JavaScript 的应用改造成 TypeScript 的过程中,我们不得不借助 any 来选择性添加和忽略对某些 JavaScript 模块的静态类型检测,直至逐步替换掉所有的 JavaScript。
any 类型会在对象的调用链中进行传导,即所有 any 类型的任意属性的类型都是 any,如下代码所示:
let anything: any = {};
let z = anything.x.y.z; // z 类型是 any,不会提示错误
z(); // 不会提示错误
如果一个 TypeScript 应用中充满了 any,此时静态类型检测基本起不到任何作用,也就是说与直接使用 JavaScript 没有任何区别。因此,除非有充足的理由,否则我们应该尽量避免使用 any ,并且开启禁用隐式 any 的设置。
unknown 是 TypeScript 3.0 中添加的一个类型,它主要用来描述类型并不确定的变量。
比如在多个 if else 条件分支场景下,它可以用来接收不同条件下类型各异的返回值的临时变量,如下代码所示:
let result: unknown;
if (x) {
result = x();
} else if (y) {
result = y();
} ...
在 3.0 以前的版本中,只有使用 any 才能满足这种动态类型场景。
与 any 不同的是,unknown 在类型上更安全。比如我们可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any,如下代码所示:
let result: unknown;
let num: number = result; // 提示 ts(2322)
let anything: any = result; // 不会提示错误
使用 unknown 后,TypeScript 会对它做类型检测。但是,如果不缩小类型(Type Narrowing),我们对 unknown 执行的任何操作都会出现如下所示错误:
let result: unknown;
result.toFixed(); // 提示 ts(2571)
而所有的类型缩小手段对 unknown 都有效,如下代码所示:
let result: unknown;
if (typeof result === "number") {
result.toFixed(); // 此处 hover result 提示类型是 number,不会提示错误
}
void 仅适用于表示没有返回值的函数。即如果该函数没有返回值,那它的类型就是 void。
在 strict 模式下,声明一个 void 类型的变量几乎没有任何实际用处,因为我们不能把 void 类型的变量值再赋值给除了 any 和 unkown 之外的任何类型变量。反过来仅 any、never、undefined 可以赋值给 void
它们是 TypeScript 值与类型关键字同名的唯二例外
let undeclared: undefined = undefined; // 鸡肋
let nullable: null = null; // 鸡肋
undefined 的最大价值主要体现在接口类型上,它表示一个可缺省、未定义的属性。
我们可以把 undefined 值或类型是 undefined 的变量赋值给 void 类型变量,反过来,类型是 void 但值是 undefined 的变量不能赋值给 undefined 类型。
const userInfo: {
id?: number;
} = {};
let undeclared: undefined = undefined;
let unusable: void = undefined;
unusable = undeclared; // ok
undeclared = unusable; // ts(2322)
null ,它表明对象或属性可能是空值。尤其是在前后端交互的接口,比如 Java Restful、Graphql,任何涉及查询的属性、对象都可能是 null 空对象,如下代码所示:
const userInfo: {
name: null | string;
} = { name: null };
除此之外,undefined 和 null 类型还具备警示意义,它们可以提醒我们针对可能操作这两种(类型)值的情况做容错处理。
我们需要类型守卫(Type Guard)在操作之前判断值的类型是否支持当前的操作。类型守卫既能通过类型缩小影响 TypeScript 的类型检测,也能保障 JavaScript 运行时的安全性,如下代码所示:
const userInfo: {
id?: number;
name?: null | string;
} = { id: 1, name: "Captain" };
if (userInfo.id !== undefined) {
// Type Guard
userInfo.id.toFixed(); // id 的类型缩小成 number
}
不建议随意使用非空断言来排除值可能为 null 或 undefined 的情况,因为这样很不安全。
userInfo.id!.toFixed(); // ok,但不建议
userInfo.name!.toLowerCase(); // ok,但不建议
而比非空断言更安全、类型守卫更方便的做法是使用单问号(Optional Chain)、双问号(空值合并),我们可以使用它们来保障代码的安全性,如下代码所示:
userInfo.id?.toFixed(); // Optional Chain
const myName = userInfo.name ?? `my name is ${info.name}`; // 空值合并
never 表示永远不会发生值的类型,这里我们举一个实际的场景进行说明。
首先,我们定义一个统一抛出错误的函数,代码示例如下(圆括号后 : + 类型注解 表示函数返回值的类型)
function ThrowError(msg: string): never {
throw Error(msg);
}
以上函数因为永远不会有返回值,所以它的返回值类型就是 never。
同样,如果函数代码中是一个死循环,那么这个函数的返回值类型也是 never
function InfiniteLoop(): never {
while (true) {}
}
let Unreachable: never = 1; // ts(2322)
Unreachable = "string"; // ts(2322)
Unreachable = true; // ts(2322)
let num: number = Unreachable; // ok
let str: string = Unreachable; // ok
let bool: boolean = Unreachable; // ok
但是反过来,除了 never 自身以外,其他类型(包括 any 在内的类型)都不能为 never 类型赋值。
在恒为 false 的类型守卫条件判断下,变量的类型将缩小为 never(never 是所有其他类型的子类型,所以是类型缩小为 never,而不是变成 never)。因此,条件判断中的相关操作始终会报无法更正的错误(我们可以把这理解为一种基于静态类型检测的 Dead Code 检测机制),如下代码所示:
const str: string = "string";
if (typeof str === "number") {
str.toLowerCase(); // Property 'toLowerCase' does not exist on type 'never'.ts(2339)
}
基于 never 的特性,我们还可以使用 never 实现一些有意思的功能。比如我们可以把 never 作为接口类型下的属性类型,用来禁止写接口下特定的属性,示例代码如下:
const props: {
id: number;
name?: never;
} = {
id: 1,
};
props.name = null; // ts(2322))
props.name = "str"; // ts(2322)
props.name = 1; // ts(2322)
此时,无论我们给 props.name 赋什么类型的值,它都会提示类型错误,实际效果等同于 name 只读 。
函数类型
在 JavaScript 中,函数是构建应用的一块基石,我们可以使用函数抽离可复用的逻辑、抽象模型、封装过程。在 TypeScript 中,虽然有类、命名空间、模块,但是函数同样是最基本、最重要的元素之一。
// 在 TypeScript 里,可以通过 function 字面量和箭头函数的形式定义函数
function add() {}
const add = () => {};
// 可以显式指定函数参数和返回值的类型
const add = (a: number, b: number): number => {
return a + b;
};
返回值类型
在 JavaScript 中,一个函数可以没有显式 return,此时函数的返回值应该是 undefined
function fn() {
// TODO
}
console.log(fn()); // => undefined
在 TypeScript 中,如果显式声明函数的返回值类型为 undfined,将会得到如下所示的错误提醒。
function fn(): undefined {
// ts(2355) A function whose declared type is neither 'void' nor 'any' must return a value
// TODO
}
正确的做法是使用 void 类型来表示函数没有返回值的类型
function fn1(): void {}
fn1().doSomething(); // ts(2339) Property 'doSomething' does not exist on type 'void'.
我们可以使用类似定义箭头函数的语法来表示函数类型的参数和返回值类型,此时=> 类型仅仅用来定义一个函数类型而不用实现这个函数。
需要注意的是,这里的=>与 ES6 中箭头函数的=>有所不同。TypeScript 函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>是函数的实现。
如下示例中,我们定义了一个函数类型(这里我们使用了类型别名 type),并且使用箭头函数实现了这个类型。
type Adder = (a: number, b: number) => number; // TypeScript 函数类型定义
const add: Adder = (a, b) => a + b; // ES6 箭头函数
注意:右侧的箭头函数并没有显式声明类型注解,不过可以根据上下文类型进行推断。
在对象(即接口类型)中,除了使用这种声明语法,我们还可以使用类似对象属性的简写语法来声明函数类型的属性,如下代码所示:
interface Entity {
add: (a: number, b: number) => number;
del(a: number, b: number): number;
}
const entity: Entity = {
add: (a, b) => a + b,
del(a, b) {
return a - b;
},
};
在某种意义上来说,这两种形式都是等价的。但是很多时候,我们不必或者不能显式地指明返回值的类型。