一文吃透 TypeScript 核心类型与语法,前端进阶必备!

112 阅读13分钟

any、unknown、never

any 类型

  • any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值
  • any 的使用场景
    1. 特殊场景需要关闭变量的类型检查,使用 any
    2. 迁移老 js 项目,先设置为 any,再一步一步迁移到 ts
  • 从集合论的角度讲,any 类型可以看作其他类型的全集,ts 将其叫做"顶层类型"
  • 类型推断
    • 如果 ts 的自动类型推断无法推断出变量的类型,就会将其推断为 any 类型
    • 如果想要阻止推断为 any 类型,可以开启 noImplicitAny(不能存在隐式 any

unknown 类型

  • unknownany 的相似之处,在于所有类型的值都可以分配给 unknown 类型
  • unknown 类型跟 any 类型的不同之处在于,它不能直接使用,主要有以下几个限制
    • unknown 类型的变量,不能直接赋值给其他类型的变量(除了 any 类型和 unknown 类型)
    • 不能直接调用 unknown 类型变量的方法和属性
    • unknown 类型变量能够进行的运算是有限的,只能进行比较运算(运算符 =====!=!==||&&?)、取反运算(运算符 !)、typeof 运算符和 instanceof 运算符这几种,其他运算都会报错
  • unknown 类型的使用需要经过"类型缩小",缩小 unknown 变量的类型范围,确保不会出错
let a: unknown = 1;

// 缩小为 number 类型
if (typeof a === "number") {
	a.toFixed(1);
}

// 缩小为 string 类型
if (typeof a === "string") {
	a.trim();
}
  • 在集合论上,unknown 也可以视为所有其他类型(除了 any)的全集,所以它和 any 一样,也属于 TypeScript 的顶层类型

never 类型

  • 为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值
  • 使用场景
    • 在一些类型运算之中,保证类型运算的完整性(如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于 never 类型)
    • 不可能返回值的函数,返回值的类型就可以写成 never
  • never 可以赋值给任意类型,因为在集合论中,never 为空集,空集是所有集合的子集,所以可以赋值
  • never 是一个底层类型

基本类型和包装类型

ts 中的基本类型和 js 的基本类型是一样的

  • boolean 包含 truefalse
  • string 字符串
  • number 数字类型,包括整数和浮点数
  • bigint 大整数
  • symbol
  • object 包括对象、数组、函数
  • undefinednull,各自代表一种类型
    • 在关闭 noImplicitAnystrictNullChecks 的时候,如果变量设置为 undefined 或者 null,变量类型为 any,因此需要开启

包装类型

js 的8种类型,undefinednull 是两个特殊值,object 属于复合类型,剩下的5种属于原始类型,代表最基本的,不可再分的值

这5种基本类型,又有对应的包装类型,而 Symbolbigint 不能作为构造函数使用,也就没有对应的包装类型,所以 ts 中基本类型的包装类型包括以下

  • Boolean
  • String
  • Number

包装类型和字面量类型 TypeScript 对五种原始类型分别提供了大写和小写两种类型。

  • Boolean 和 boolean
  • String 和 string
  • Number 和 number
  • BigInt 和 bigint
  • Symbol 和 symbol 其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象

注意:symbol 和 bigint 的大小写没有区别,原因上面解释了 (不能作为构造函数使用)

Objectobject

大写 Object

  • 大写的 Object 类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是 Object 类型
  • 除了 undefinednull 这两个值不能转为对象,其他任何值都可以赋值给 Object 类型
  • 空对象 {}Object 类型的简写形式

小写 object

  • 小写的 object 类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值

undefinednull

undefinednull 既是值,又是类型。

  • 作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为 undefinednull。但是可能导致无法取到变量上的一些属性和方法。
  • 因此提供了 strictNullChecks 选项,让 undefinednull 不能赋值给其他类型变量,防止报错

值类型

TypeScript 规定,单个值也是一种类型,称为“值类型”。

TypeScript 推断类型时,遇到 const 命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。

// x 的类型是 "https"
const x = 'https';

只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。

联合类型 (并集)

联合类型:一个值可以是多种类型中的任意一种(“或”的关系)

联合类型(union types)指的是多个类型组成的一个新类型,使用符号 | 表示。

联合类型 A|B 表示,任何一个类型只要属于 AB,就属于联合类型 A|B

类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。

交叉类型 (交集)

交叉类型:一个值必须同时满足所有类型(“且”的关系),拥有所有成员

交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号 & 表示。

交叉类型 A&B 表示,任何一个类型必须同时属于 AB,才属于交叉类型 A&B,即交叉类型同时满足 AB 的特征。

交叉类型的主要用途是表示对象的合成。

数组

声明方式

  1. 第一种
let arr: number[] = [1,2,3]
  1. 第二种
let arr: Array<number> = [1,2,3]

只读数组

声明方式:

  1. 使用 readonly 关键字
const arr: readonly number[] = [0, 1]
  1. 使用只读泛型
const arr: Readonly<number[]> = [0, 1]
const arr: ReadonlyArray<number[]> = [0, 1]
  1. 使用 const 断言
const arr = [0, 1] as const;

元组

它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。

比如:

const s:[string, string, boolean] = ['a', 'b', true];

元组有以下语法:

  1. 元组成员的类型可以添加问号后缀(?),表示该成员是可选的。

注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。

let a:[number, number?] = [1];
  1. 使用 ... 拓展运算符,可以创建表示无限成员数量的元组

扩展运算符(...)用在元组的任意位置都可以,它的后面只能是一个数组或元组

type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];
  1. 元组的成员可以添加成员名,这个成员名是说明性的,可以任意取名,没有实际作用。
type Color = [
  red: number,
  green: number,
  blue: number
];

const c:Color = [255, 255, 255];
  1. 元组可以通过方括号,读取成员类型。由于元组的成员都是数值索引,即索引类型都是 number,所以可以像下面这样读取。
type Tuple = [string, number];
type Age = Tuple[1]; // number

type Tuple = [string, number, Date];
type TupleEl = Tuple[number];  // string|number|Date
  1. 只读元组,语法和只读数组一样,这里不重复了

  2. 成员数量的推断

正常使用元组可以推断出成员数量,但是如果使用了可选成员或者拓展运算符,就获取不到成员数量,会爆 ts 错误

对象

ts 中对象包括以下常用语法

  1. 可选属性,如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。
type User = {
  firstName: string;
  lastName?: string;
};

// 等同于
type User = {
  firstName: string;
  lastName: string|undefined;
};

使用方式

// 写法一
let firstName = (user.firstName === undefined)
  ? 'Foo' : user.firstName;
let lastName = (user.lastName === undefined)
  ? 'Bar' : user.lastName;

// 写法二
let firstName = user.firstName ?? 'Foo';
let lastName = user.lastName ?? 'Bar';
  1. 只读属性,属性名前面加上 readonly 关键字,表示这个属性是只读属性,不能修改。

  2. 属性名索引,除了 string,还可以是 numbersymbol

type MyObj = {
  [property: string]: string
};
  1. 解构赋值声明类型,只能给整体声明类型,不能给单个变量指定类型

  2. “结构类型“原则,只要对象 B 满足对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则。比较抽象,反正大概就是下面这样的代码

const B = {
  x: 1,
  y: 1
};

const A:{ x: number } = {
    x: 1,
    y: 1 // 字面量方式声明报错
}

const A: {x: number } = B // 变量方式赋值不报错
  1. 严格字面量检查,如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。刚好和"解构类型"原则相反
const point:{
  x:number;
  y:number;
} = {
  x: 1,
  y: 1,
  z: 1 // 报错
};
  1. 最小可选属性原则,如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”。

函数

ts 函数类型的两种声明方式

  1. 普通函数的类型声明,类型直接写在函数上
function add(x: number, y: number): number {
	return x + y
}
  1. 箭头函数的声明方式,也有三种方式
// 方式1
const add = (x: number, y: number): number => {
  return x + y
}

// 方式2
const add: (x: number, y: number) => number = (x, y) => {
  return x + y
}

// 方式3
type Add = (x: number, y: number) => number
const add: Add = (x, y) => {
  return x + y
}

函数也可以使用 typeof 关键字获取类型

function add(x: number, y: number): number {
  return x + y
}

const add2: typeof add = (x, y) => {
  return x + y
}

可选参数

function f(x?:number) {
  // ...
}
f() // 正确

function f(x:number|undefined) {
  return x;
}
f() // 错误 如果是这样定义,需要传入undefined

参数默认值,设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值。

function createPoint(
  x:number = 0,
  y:number = 0
):[number, number] {
  return [x, y];
}

createPoint() // [0, 0]

参数解构,写法如下

function f(
  [x, y]: [number, number]
) {
  // ...
}

function sum(
  { a, b, c }: {
     a: number;
     b: number;
     c: number
  }
) {
  console.log(a + b + c);
}

// 默认值写法
function add({ x = 10, y = 10 }: { x?: number, y?: number }): number {
  return x + y
}

console.log(add({}))

rest 参数,rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。

// rest 参数为数组
function joinNumbers(...nums:number[]) {
  // ...
}

// rest 参数为元组
function f(...args:[boolean, number]) {
  // ...
}

// 最后一个参数可选
function f(
  ...args: [boolean, string?]
) {}

// 利用灵活的元组的rest参数
function f(...args:[boolean, ...string[]]) {
  // ...
}

f(false, '1', '2', '4')

readonly 只读参数

function arraySum(
  arr:readonly number[]
) {
  // ...
  arr[0] = 0; // 报错
}

void 类型void 类型表示函数没有返回值,并不是说函数不返回值,而是说返回值不重要

type voidFunc = () => void;
 
const f:voidFunc = () => {
  return 123; // 不报错
};

function f():void {
  return true; // 报错
}
 
const f3 = function ():void {
  return true; // 报错
};

never 类型,表示肯定不会出现的值,比如函数的返回值,包括

  • 抛出错误的函数
  • 无限执行的函数 never 类型不同于 void 类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回 undefined

高阶函数,一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)


函数重载,有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为

function reverse(str:string):string;
function reverse(arr:any[]):any[];
function reverse(
  stringOrArray:string|any[]
):string|any[] {
  if (typeof stringOrArray === 'string')
    return stringOrArray.split('').reverse().join('');
  else
    return stringOrArray.slice().reverse();
}

构造函数的类型

  • 构造函数的类型写法,就是在参数列表前面加上 new 命令
  • 构造函数还有另一种类型写法,就是采用对象形式
  • 某些函数既是构造函数,又可以当作普通函数使用,比如 Date(),可以两种函数的类型定义可以写在对象里
class Foo {
	b: string;
}

// 构造函数的类型
type FooContructor = new () => Foo;

// 构造函数类型的对象写法
type FooContructor = {
  // 构造函数的定义
  new (): Foo;
  // 普通函数的定义
  (): string;
};

function create(c: FooContructor): Foo {
	return new c();
}

const foo = create(Foo);

enum 类型

Enum 结构,用来将相关常量放在一个容器里面,方便使用,默认如果不赋初始值,枚举值将依次增加,比如

enum Color {
	Red, // 0
	Green, // 1
	Blue // 2
}

Enum 结构本身也是一种类型。比如,上例的变量 c 等于 1,它的类型可以是 Color,也可以是 number

let c:Color = Color.Green; // 正确
let c:number = Color.Green; // 正确

Enum 编译之后会变成 JS 代码,这是一个很特别的点,因为 TS 的定位是 JS 语言的类型增强,所以谨慎使用 Enum 结构,很大程度上,Enum 可以被对象的 as const 断言替代,它也可以作为类型

enum Foo {
  A,
  B,
  C,
}

const Bar = {
  A: 0,
  B: 1,
  C: 2,
} as const;

// 对象的as const 断言写法也可以作为类型,即使用 typeof
const Bar = {
	A: 0,
	B: 1,
	C: 2,
} as const;

const a: typeof Bar = { 
  A: 0,
}

Enum 之前也可以加上

注意:TypeScript 5.0 之前,Enum 有一个 Bug,就是 Enum 类型的变量可以赋值为任何数值。

enum Bool {
  No,
  Yes
}

function foo(noYes:Bool) {
  // ...
}

foo(33);  // TypeScript 5.0 之前不报错

enum 的值

enum 支持字符串和数字两种,数字可以是除了 bigint 之外的所有数字

对于数字类型的枚举来说,enum 支持反向映射 (即反向查找),而字符串类型不行

enum A {
  white=4
}

console.log(A[4]) // "white"

enum 的合并

多个同名的 Enum 结构会自动合并 作用:

  • 补充外部定义的 Enum 结构 注意事项:
  • Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
  • 同名 Enum 合并时,不能有同名成员,否则报错。
  • 同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。

反向映射

即使用数字类型的 enum,不仅可以通过键找到值,还可以通过值找到键

enum A {
	Red,
	Blue,
	Green,
}

console.log(A.Red) // 0
console.log(A[0]); // Red

eunm 的本质

enum 本质是将枚举编译成了一个对象

比如如下代码

enum A {
  Red,
  Blue=2,
  Green
}

console.log(A)

// 打印结果为:
/**
{
  "0": "Red",
  "2": "Blue",
  "3": "Green",
  "Red": 0,
  "Blue": 2,
  "Green": 3
} 
*/

enumconst enum 的编译区别是

// ts代码
const enum A {
	Red,
	Blue,
	Green,
}

console.log(A.Red)
console.log(A.Blue)
console.log(A.Green)

// enum编译结果
var A;
(function (A) {
    A[A["Red"] = 0] = "Red";
    A[A["Blue"] = 1] = "Blue";
    A[A["Green"] = 2] = "Green";
})(A || (A = {}));
console.log(A.Red);
console.log(A.Blue);
console.log(A.Green);

// const enum 编译结果
console.log(0 /* A.Red */);
console.log(1 /* A.Blue */);
console.log(2 /* A.Green */);

其中 enum 的编译结果咋一眼看过去看不懂,实际上可以分开分析

var A // 定义了 和enum名称一样的全局变量

// 立即执行函数,对A赋值
(function (A) {
	...
})(A || (A = {})) // 如果A有值传入A(用于枚举合并的场景),否则传入兜底的空对象

// 对A键的赋值
A[A["Red"] = 0] = "Red"; 
// 分为两步分析
// 1. 包括 A["Red"] = 0  所以字符串类型的键名就获得了键值
// 2. 使用键值再进行赋值,实现反向映射,A[A["Red"] = 0] = "Red",相当于A[0] = "Red"

// 而如果是字符串作为键值,只有这一层,没有反向映射
A["Red"] = "red";