Skip to content

TypeScript 学习笔记

语言介绍

TypeScript 与 JavaScript 发展史:

TypeScript 与 JavaScript 对比:

类型的概念:类型就是人为添加的一种编程约束和用户提示

运行时做的叫做动态类型检查,运行之前的编译期做的叫做静态类型检查

动态类型检查:源码中不保留类型信息,对某个变量赋什么值、做什么操作都是允许的,写代码很灵活,但有类型不安全隐患,⽐如对 string 做了乘除,对 Date 对象调⽤了 exec ⽅法,这些都是运⾏时才能检查出来的错误

静态类型的优点:

  • 有利于代码的静态分析;
  • 有利于发现错误;
  • IDE 支持;
  • 提供代码文档;
  • 有利于重构代码;

静态类型的缺点:

  • 丧失代码灵活性;
  • 增加编程工作量;
  • 更高的学习成本;
  • 引入独立的编译步骤;
  • 兼容性问题;

基础用法

类型声明

TypeScript 规定,变量只有赋值后才能使用,否则就会报错

javascript
let x:number;
console.log(x) // 报错

类型推断

类型声明不是必须的,如果没有,TypeScript 会自己推断类型

编译

JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码。所以,TypeScript 项目要想运行,必须先转为 JavaScript 代码,这个代码转换的过程就叫做“编译”(compile)

  • 官方提供编译器;
  • 类型检查只是编译时的类型检查,而不是运行时的类型检查;
  • 一旦代码编译为 JavaScript,运行时就不再检查类型;

值与类型

TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成

TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。前者使用 JavaScript 语法,后者使用 TypeScript 的类型语法 它们是可以分离的,TypeScript 的编译过程,实际上就是把 “类型代码”全部拿掉,只保留“值代码”

TypeScript Playground

官方在线编译练习页面: TS Playground - An online editor for exploring TypeScript and JavaScript

tsc 编译器

TypeScript 官方提供的编译器叫做 tsc,可以将 TypeScript 脚本编译成 JavaScript 脚本

TypeScript 脚本文件使用 .ts 后缀名,JavaScript 脚本文件使用 .js 后缀名。tsc 的作用就是把 .ts 脚本转变成 .js 脚本

tsc 使用

javascript
// 安装
npm install -g typescript

// 查看版本
tsc -v

// 查看帮助
tsc --help

// 编译某个文件
tsc app.ts

// 命令参数,制定编译后 JavaScript 版本
tsc --target es2015 app.ts

// 编译报错停止生成对应文件的参数
tsc --noEmitOnError app.ts

// 只校验类型是否正确,不会生成 JavaScript 文件
tsc --noEmit app.ts

tsconfig.json 配置

ts-node 模块:便捷方式运行 TypeScript 代码查看结果 ts-node 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码

三种特殊类型

any

表示没有任何限制,该类型的变量可以赋予任意类型的值 特点:

  • 相当于关闭这个类型检查;
  • 顶层类型(top type);
  • 污染其他变量(它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错);

使用场景:

  • 出于特殊原因,需要关闭某些变量的类型检查;
  • 适配老项目,快速迁移;

类型推断为 any 就会报错的配置:

javascript
tsc --noImplicitAny app.ts

但有个隐患:使用 let 和 var 命令声明变量,但不赋值也不指定类型,是不会报错的

javascript
let x;

x = 123;
x = { foo: 'hello' };

所以:建议使用 let 和 var 声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患

javascript
let x:any = 'hello';
let y:number;

y = x; // 不报错

y * 123 // 不报错
y.toFixed() // 不报错

污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用 any 类型的另一个主要原因

unknown

表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像 any 那样自由,可以视为严格版的 any

凡是需要设置 any 类型的地方都应该优先考虑设为 unknown 类型、除了 any 外其他所有类型的全集

跟 any 相似之处:所有类型的值都可以分配给 unknown 类型、顶层类型

javascript
let x:unknown;

x = true; // 正确
x = 42; // 正确
x = 'Hello World'; // 正确

跟 any 不同之处:

  • 不能直接使用——unknown 类型的变量,不能直接赋值给其他类型的变量(除了 any 类型和 unknown 类型),避免污染变量问题,从而跟 any 类型不一样
javascript
let v:unknown = 123;

let v1:boolean = v; // 报错
let v2:number = v; // 报错
  • 不能直接调用 unknown 类型的变量的方法和属性
javascript
let v1:unknown = { foo: 123 };
v1.foo  // 报错

let v2:unknown = 'hello';
v2.trim() // 报错

let v3:unknown = (n = 0) => n + 1;
v3() // 报错
  • unknown 类型能够进行的运算是有限的,只能进行比较运算(运算符:==、===、!=、!==、||、&&、?)、取反运算(运算符:!)、typeof 运算符合 instanceof 运算符这几种
javascript
let a:unknown = 1;

a + 1 // 报错
a === 1 // 正确

使用 unknown 类型变量的方法:类型缩小——缩小 unknown 变量的类型范围,确保不会出错(明确 unknown 类型变量的实际类型,才允许使用它,防止像 any 一样乱用,变量污染)

javascript
let a:unknown = 1;

// 类型缩小
if (typeof a === 'number') {
  let r = a + 10; // 正确
}

if (typeof a === 'string') {
  a.length; // 正确
}

never

不存在任何属于“空类型”的值,这样的类型的成为 never,即不可能有这样的值 特点:

  • never 类型的变量可以赋值给任意类型(空集是任何集合的子集);
  • 唯一一个底层类型;

使用场景:一些类型运算中,保证类型运算的完整性

javascript
function fn(x:string|number) {
  if (typeof x === 'string') {
    // ...
  } else if (typeof x === 'number') {
    // ...
  } else {
    x; // never 类型
  }
}

基本类型

JavaScript 语言(注意,不是 TypeScript)将值分成 8 种类型。

  • boolean
  • string
  • number
  • bigint
  • symbol

以上 5 种为原始类型,其中 symbol、bigint 类型无法获取它们的包装对象(即 Symbol() 和 BigInt() 不能作为构造函数使用)

  • object(复合类型:对象、数组和函数)
  • undefined(特殊值类型)
  • null(特殊值类型)

注意类型名称都是小写 特殊:undefined 和 null 即可以作为值,也可以作为类型,取决于怎么使用他们

注意:

  • bigint 与 number 类型不兼容
javascript
const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错
// bigint 赋值给整数和小数都会报错
  • bigint 类型是 ES2020 标准引入的。如果使用这个类型,TypeScript 编译的目标 JavaScript 版本不能低于 ES2020(即编译参数 target 不低于 es2020)

如果没有声明类型的变量,被赋值为 undefined 或 null,在关闭编译设置 noImplicitAny 和 strictNullChecks 时,它们的类型会被推断为 any

javascript
// 关闭 noImplicitAny 和 strictNullChecks

let a = undefined;   // any
const b = undefined; // any

let c = null;        // any
const d = null;      // any

包装对象类型

包装对象:指的是这些类型变量(boolean、string、number)需要时,会自动产生的对象

javascript
'hello'.charAt(1) // 'e'

包装对象类型与字面量类型

javascript
'hello' // 字面量
new String('hello') // 包装对象

Boolean 和 boolean
String 和 string
Number 和 number
BigInt 和 bigint
Symbol 和 symbol

const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确

const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错

大写类型包含包装类型和字面量类型,小写类型只包含字面量,不包含包装对象

建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错

Object 类型与 object 类型

Object 广义对象:所有可以转成对象的值,都是 Object 类型,几乎所有的值{} (空对象)为 Object 类型的简写形式

javascript
let obj:Object;
// 或 let obj: {}

obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

obj = undefined; // 报错
obj = null; // 报错

object 侠义对象:字面量表是,只包含对象、数组和函数,不包括原始类型的值

javascript
let obj:object;

obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错

使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型 object,不使用大写类型 Object

注意,无论是大写的 Object 类型,还是小写的 object 类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中

javascript
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };

o1.toString() // 正确
o1.foo // 报错

o2.toString() // 正确
o2.foo // 报错

undefined 和 null 的特殊性

undefined 和 null 即是值,又是类型 作为值:任何其他类型的变量都可以赋值为 undefined 或 null

javascript
let age:number = 24;

age = null;      // 正确
age = undefined; // 正确

并不是类型里面包含了 undefined 或 null,而是故意这样设计的,目的为了跟 JavaScript 行为保持一致

JavaScript 的行为是,变量如果等于 undefined 就表示还没有赋值,如果等于 null 就表示值为空。所以,TypeScript 就允许了任何类型的变量都可以赋值为这两个值

TypeScript 提供了一个编译选项 strictNullChecks。只要打开这个选项,undefined 和null 就不能赋值给其他类型的变量(除了 any 类型和 unknown 类型),而且 undefined 和 null 这种值不能相互赋值,但是赋值给 any 或 unknown 却没有限制

值类型

typescript
let x:'hello';

x = 'hello'; // 正确
x = 'world'; // 报错

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

// y 的类型是 string
const y:string = 'https';

// x 的类型是 { foo: number }
const x = { foo: 1 };
// JavaScript 里面,const变量赋值为对象时,属性值是可以改变的

const x:5 = 4 + 1; // 报错

联合类型

typescript
let setting:true|false;

let gender:'male'|'female';

let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';

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

typescript
function printId(
  id:number|string
) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

function getPort(
  scheme: 'http'|'https'
) {
  switch (scheme) {
    case 'http':
      return 80;
    case 'https':
      return 443;
  }
}

交叉类型

指的多个类型组成的一个新类型,使用符号 & 表示

typescript
let obj:
  { foo: string } &
  { bar: string };

obj = {
  foo: 'hello',
  bar: 'world'
};

type A = { foo: number };

type B = A & { bar: number };

type 命令

type 命令用来定义一个类型的别名 别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型

特点:

  • 别名不允许重名;
  • 别名的作用域是块级作用域;
  • 别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套
  • type 命令属于类型相关代码,编译 JavaScript 的时候会全部删除
typescript
type Color = 'red';
type Color = 'blue'; // 报错

type Color = 'red';

if (Math.random() < 0.5) {
  type Color = 'blue'; // 作用域问题不会报错
}

type World = "world";
type Greeting = `hello ${World}`; // 别名嵌套

typeof 运算符

JavaScript 语言中,typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型

typescript
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n // "bigint"

同一段代码可能存在两种 typeof 运算符,一种用在值相关的 JavaScript 代码部分(值运算),另一种用在类型相关的 TypeScript 代码部分(类型运算)

由于编译时不会进行 JavaScript 的值运算,所以TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式

typescript
type T = typeof Date(); // 报错

typeof 命令的参数不能是类型

typescript
type Age = number
type MyAge = typeof Age // 报错

块级类型声明

TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效(块级作用域

typescript
if (true) {
  type T = number;
  let v:T = 5;
} else {
  type T = string; // 可以重复声明
  let v:T = 'hello';
}

类型的兼容

TypeScript 为这种情况定义了一个专门术语。如果类型 A 的值可以赋值给类型 B,那么类型 A 就称为类型 B 的子类型(subtype)

TypeScript 的一个规则是,凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行

数组

两种声明写法

typescript
// 字面量定义
const arr: number[] = [1]

// 泛型定义
const arr1: Array<number | string> = [1, '2']

// 注意联合类型的使用,需要搭配括号,跟优先级有关
let arr:(number|string)[];

TypeScript 允许使用方括号读取数组成员的类型

typescript
type Names = string[];
type Name = Names[0]; // string
// 也可以写成这样
type Name = Names[number]

数组类型推断 前提条件是初始值为空的数组 如果没有声明数组类型,数组会根据添加的元素进行类型推断

typescript
const arr = [];
arr // 推断为 any[]

arr.push(123);
arr // 推断类型为 number[]

arr.push('abc');
arr // 推断类型为 (string|number)[]

// 注意⚠️:类型不为空的数组
// 推断类型为 number[]
const arr = [123];

arr.push('abc'); // 报错

只读数组,const 断言

typescript
// 只读数组与数组的父类型关系

function getSum(s:number[]) {
  // ...
}

const arr:readonly number[] = [1, 2, 3];

getSum(arr) // 报错

// 修改
getSum(arr as number[])

注意⚠️:readonly 与数组的泛型写法不能一起使用

typescript
// 报错
const arr:readonly Array<number> = [0, 1];

// TypeScript 专门的泛型
const a1:ReadonlyArray<number> = [0, 1];

const a2:Readonly<number[]> = [0, 1];

通过使用 const 断言来使用只读数组

typescript
const arr = [0, 1] as const
arr[0] = 2 // 报错

多维数组

typescript
// 数组成员类型为:number
var multi:number[][] =
  [[1,2,3], [23,24,25]];

元组

成员类型写在方括号里面的就是元组,写在外面的就是数组

typescript
// 数组
let a:number[] = [1];

// 元组
let t:[number] = [1];

元组必须显式给出类型声明 元组成员的类型可以添加问号后缀(?),表示该成员是可选的

typescript
let a:[number, number?] = [1];

注意⚠️:可选成员必须放在尾部(必须放在必选成员之后)

元组的成员是有限的,越界的成员会报错,但是可以通过扩展符使用不限制成员数量

typescript
type NamedNums = [
  string,
  ...number[]
];

const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];

// 扩展符的使用
type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];

如果不确定元组成员的类型和数量

typescript
type Tuple = [...any[]]

元组的成员可以添加成员名,这个成员名是说明性的,可以任意取名,没有实际作用

typescript
type Color = [
  red: number,
  green: number,
  blue: number
];

const c:Color = [255, 255, 255];

元组可以通过方括号,读取成员类型

typescript
type Tuple = [string, number];
type Age = Tuple[1]; // number

// 获取所有成员的类型
type Tuple = [string, number, Date];
type TupleEl = Tuple[number];  // string|number|Date

只读元组

typescript
// 写法一
type t = readonly [number, string]

// 写法二
type t = Readonly<[number, string]>

只读元组不能替代元组

typescript
function distanceFromOrigin([x, y]:[number, number]) {
  return Math.sqrt(x**2 + y**2);
}

let point = [3, 4] as const;

distanceFromOrigin(point); // 报错

// 使用断言解决
distanceFromOrigin(point as [number, number])

成员数量的推断

如果没有可选成员和扩展运算符,TypeScript 会推断出元组的成员数量(即元组长度)

typescript
function f(
  point:[number, number?, number?]
) {
  if (point.length === 4) {  // 报错
    // ...
  }
}

注意⚠️:使用了扩展符就将无法退出成员的数量(TypeScript 内部会把元组当做数组处理)

typescript
const myTuple:[...string[]]
  = ['a', 'b', 'c'];

if (myTuple.length === 4) { // 正确
  // ...
}

扩展运算符与成员数量

扩展运算符(...)将数组(注意,不是元组)转换成一个逗号分隔的序列,这时 TypeScript 会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的

typescript
const arr = [1, 2];

function add(x:number, y:number){
  // ...
}

add(...arr) // 报错

解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符(补充类型注解)

typescript
// 将上面的写法改成这样
const arr:[number, number] = [1, 2];

function add(x:number, y:number){
  // ...
}

add(...arr) // 正确

还有一种更简单的写法,使用断言 const (值类型)

typescript
const arr = [1, 2] as const

既可以当做数组,也可以当做元组使用

Symbol 类型

Symbol 是 ES2015 新引入的一种原始类型的值。它类似于字符串,但是每一个 Symbol 值都是独一无二的,与其他任何值都不相等

使用

typescript
let x:symbol = Symbol();
let y:symbol = Symbol();

x === y // false

symbol 类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值 unique symbol 表示单个的、某个具体的 Symbol 值,该类型声明的变量不能修改,只能用 const 声明

typescript
// 正确
const x:unique symbol = Symbol();

// 报错
let y:unique symbol = Symbol();

const 命令为变量赋值 Symbol 值时,变量类型默认就是 unique symbol,所以类型可以省略不写

typescript
const x:unique symbol = Symbol();
// 等同于
const x = Symbol();

注意⚠️:声明两个都是 unique symbol 的类型变量,值类型都是不相同的,也不能相互赋值

typescript
const a:unique symbol = Symbol();
const b:unique symbol = a; // 报错

写成相同的类型声明:

typescript
const a:unique symbol = Symbol();
const b:typeof a = a; // 正确

Symbol.for() 的使用可以创建声明不同类型的但值相等的变量

typescript
const a:unique symbol = Symbol.for('foo');
const b:unique symbol = Symbol.for('foo');

unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行

typescript
const a:unique symbol = Symbol();

const b:symbol = a; // 正确

const c:unique symbol = b; // 报错

unique symbol 类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol,不能是 symbol

typescript
const x:unique symbol = Symbol();
const y:symbol = Symbol();

interface Foo {
  [x]: string; // 正确
  [y]: string; // 报错
}

unique symbol 类型也可以用作类(class)的属性值,但只能赋值给类的 readonly static 属性

typescript
class C {
  static readonly foo:unique symbol = Symbol();
}

类型推断中有一个注意点:如果 const 变量赋值给另一个 symbol 类型的变量,则推断类型为 symbol,而不是 unique symbol

typescript
let x = Symbol();

// 类型为 symbol
const y = x;

const x1 = Symbol();

// 类型为 symbol
let y1 = x1;

总结

  • Symbol 值是独一无二的,每个值都是不相等的;
  • unique symbol 表示单个的、某个具体的 Symbol 值,声明后不能修改,只能用 const 声明,是 symbol 的子类型。使用场景:用做属性名;
  • 默认类型推断 let 声明的 symbol 变量跟 const 声明的 symbol,之间赋值都会被推断为 symbol 类型;

函数类型

基本使用

函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型

typescript
function hello(
  txt:string
):void {
  console.log('hello ' + txt);
}

注意:返回值的类型通常不用写,TypeScript 默认可以推断出来。但如果防止返回值类型被修改或者为了文档考虑,建议写上

如果一个变量被赋值为一个函数,声明写法有两种:

typescript
// 写法一
const hello = function (txt:string) {
  console.log('hello ' + txt);
}

// 写法二
const hello:
  (txt:string) => void
= function (txt) {
  console.log('hello ' + txt);
};

函数类型里面的参数名与实际参数名,可以不一致

typescript
let f:(x:number) => number;

f = function (y:number) {
  return y;
};

函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数

typescript
let myFunc:
  (a:number, b:number) => number;

myFunc = (a:number) => a; // 正确

myFunc = (
  a:number, b:number, c:number
) => a + b + c; // 报错

如果一个变量要套用另一个函数类型,有一个小技巧,就是使用 typeof 运算符(返回类型),注意:这是一个很有用的技巧,任何需要类型的地方,都可以使用 typeof 运算符从一个值获取类型

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

const myAdd:typeof add = function (x, y) {
  return x + y;
}

函数类型声明还可以采用对象的写法:

typescript
let add: {
  (x: number, y: number): number
}

add = function (x, y) {
	return x + y
}

// 写法公式,注意:间隔是冒号
{
  (参数列表): 返回值
}

适用场景:声明函数属性类型

typescript
let foo: {
  (x:number): void;
  version: string
} = f;

Interface 写法,跟对象声明写法一样

typescript
interface Myfn {
  (a: number, b: number): number
}

const add: Myfn = (a, b) => a + b

Function 类型

任何函数都是属于这个类型,相当于 any 收窄为具体的函数类型,Function 类型的值都可以直接执行

typescript
function doSomething (f: Function) {
  return f(1, 2, 3)
}

注意⚠️:Function 类型的函数可以接受任意数量的参数,每个参数的类型都是 any,返回值的类型也是any,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好

箭头函数

写法:

typescript
const repeat = (str: string, times: number): string => str.repeat(times)

注意返回值的声明位置:

typescript
type Person = { name: string };

const people = ['alice', 'bob', 'jan'].map(
  (name):Person => ({name})
);

可选参数

如果函数的某个参数可以省略,则在参数名后面加问号表示

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

f(); // OK
f(10); // OK
f(undefined) // OK

**参数名带有问号,表示该参数的类型实际上是 原始类型 | undefined,它有可能为undefined,**但是类型设置为 number | undefined 表是要么传入一个数值或 undefined,如果省略就会报错 函数的可选参数只能在参数列表的尾部,跟在必选参数的后面 养成习惯:可选参数永远放在尾部 如果函数参数多个情况下,前面的参数有可能为空,只能显示注明类型包括 undefined,传参时也要传入 undefined

typescript
let myFunc:
  (
    a:number|undefined,
    b:number
  ) => number;

用到的可选参数记得判断该参数是否为 undefined,防止误用报错(建议设置默认值):

typescript
let myFunc:
  (a:number, b?:number) => number;

myFunc = function (x, y) {
  if (y === undefined) {
    return x;
  }
  return x + y;
}

参数默认值

设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值,类型声明可以根据默认值推断出来(注意⚠️:设置了默认值,参数传 undefined,也会触发默认值

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

createPoint() // [0, 0]

注意⚠️:可选参数跟默认值不能同时使用

typescript
function f(x?: number = 0) {} // 报错

具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入 undefined

typescript
function add(
  x:number = 0,
  y:number
) {
  return x + y;
}

add(1) // 报错
add(undefined, 1) // 正确

参数解构

参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些

typescript
type ABC = { a:number; b:number; c:number };

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

rest 参数

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

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

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

注意⚠️:元组需要声明每一个剩余参数的类型。如果元组里面的参数是可选的,则要使用可选参数

typescript
function f(
  ...args: [boolean, string?]
) {}

// m 是 rest 类型数组
function multiply(n:number, ...m:number[]) {
  return m.map((x) => n * x);
}

// 类型嵌套
function f1(...args:[boolean, ...string[]]) {
  // ...
}

// 结构
function repeat(
  ...[str, times]: [string, number]
):string {
  return str.repeat(times);
}

// 等同于
function repeat(
  str: string,
  times: number
):string {
  return str.repeat(times);
}

readonly 只读参数

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

void 类型

void 类型表示函数没有返回值

typescript
function f(): void {
  console.log('xx')
}

注意⚠️:允许返回 undefined 或 null

typescript
function f1(): void {
  return undefined
}

function f2(): void {
  return null
}

如果打开 strictNullChecks 编译选项,那么 void 类型只允许返回 undefined。如果返回 null,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回 undefined

注意⚠️:如果变量、对象方法、函数参数是一个返回值为 void 类型的函数,那么并不代表不能赋值为有返回值的函数。恰恰相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错(原因:这个返回值不重要或者不产生作用

typescript
type voidFunc = () => void;

const f:voidFunc = () => {
  return 123;
};

因为这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错

typescript
const src = [1, 2, 3];
const ret = [];

src.forEach(el => ret.push(el));

注意⚠️:push() 有返回值,表示插入新元素后数组的长度。但是,对于 forEach() 方法来说,这个返回值是没有作用的,根本用不到,所以 TypeScript 不会报错 反例:使用到了这个返回值

typescript
type voidFunc = () => void;

const f:voidFunc = () => {
  return 123;
};

f() * 2 // 报错

注意⚠️:这种情况仅限于变量、对象方法和函数参数,函数字面量如果声明了返回值是 void 类型,还是不能有返回值

typescript
function f():void {
  return true; // 报错
}

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

如果函数运行结果是抛出错误,也允许返回值写成 void

typescript
function throwErr():void {
  throw new Error('something wrong');
}

除了函数,其他变量声明为 void 类型没有多大意义(可以作为一个变量类型声明),因为这时只能赋值为 undefined 或者 null(假定没有打开strictNullChecks)

typescript
let foo: void = undefined
// 没有打开 strictNullChecks 的情况下
let bar: void = null

never 类型

never 类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常的执行结束,never 是唯一一个底层类型,所有其他类型都包括这个类型使用场景:

  1. 抛出错误的函数:
typescript
function fail(msg: string): never {
  throw new Error(msg)
}

注意,只有抛出错误,才是 never 类型。如果显式用 return 语句返回一个 Error 对象,返回值就不是 never 类型 另外,由于抛出错误的情况属于 never 类型或 void 类型,所以无法从返回值类型中获知,抛出的是哪一种错误

  1. 无限执行的函数:
typescript
const sing = funciton (): never {
  while(true) {
    ...
  }
}

注意⚠️:never 类型不同于 void 类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回 undefined

typescript
// 正确
function sing():void {
  console.log('sing');
}

// 报错
function sing():never {
  console.log('sing');
}

如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型就是 never。如果程序中调用了一个返回值类型为 never 的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码

typescript
function neverReturns():never {
  throw new Error();
}

function f(
  x:string|undefined
) {
  if (x === undefined) {
    neverReturns();
  }

  x; // 推断为 string
}

省略 never 类型声明的情况:一个函数在某些情况下是正常返回值的,另一些情况下会抛出错误

typescript
function sometimesThrow():number {
  if (Math.random() > 0.5) {
    return 100;
  }

  throw new Error('Something went wrong');
}

const result = sometimesThrow();

局部类型

函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型

typescript
function hello(txt:string) {
  type message = string;
  let newTxt:message = 'hello ' + txt;
  return newTxt;
}

const newTxt:message = hello('world'); // 报错

高阶函数

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

typescript
(someValue: number) => (multiplier: number) => someValue * multiplier;

函数重载

有些函数可以接受不同类型不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)

typescript
function reverse(str:string):string;
function reverse(arr:any[]):any[];

reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]

注意⚠️:到这里并没有结束,后面还必须对函数 reverse() 给予完整的类型声明

typescript
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();
}
// 前两行类型声明列举了重载的各种情况。第三行是函数本身的类型声明,它必须与前面已有的重载声明兼容
typescript
function add(
  x:number,
  y:number
):number;
function add(
  x:any[],
  y:any[]
):any[];
function add(
  x:number|any[],
  y:number|any[]
):number|any[] {
  if (typeof x === 'number' && typeof y === 'number') {
    return x + y;
  } else if (Array.isArray(x) && Array.isArray(y)) {
    return [...x, ...y];
  }

  throw new Error('wrong parameters');
}

注意⚠️:重载的各个类型描述与函数的具体实现之间,不能有其他代码,否则报错

重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明

typescript
function f(x:any):number;
function f(x:string): 0|1;
function f(x:any):any {
  // ...
}

const a:0|1 = f('hi'); // 报错

对象的方法也可以实现重载:

typescript
class StringBuilder {
  #data = '';

  add(num:number): this;
  add(bool:boolean): this;
  add(str:string): this;
  add(value:any): this {
    this.#data += String(value);
    return this;
  }

  toString() {
    return this.#data;
  }
}

函数重载可以用来精确描述函数参数与返回值之间的关系:

typescript
function createElement(
  tag:'a'
):HTMLAnchorElement;
function createElement(
  tag:'canvas'
):HTMLCanvasElement;
function createElement(
  tag:'table'
):HTMLTableElement;
function createElement(
  tag:string
):HTMLElement {
  // ...
}

可以通过对象声明表示:

typescript
type CreateElement = {
  (tag:'a'): HTMLAnchorElement;
  (tag:'canvas'): HTMLCanvasElement;
  (tag:'table'): HTMLTableElement;
  (tag:string): HTMLElement;
}

建议:一般函数重载存在比较复杂的类型声明关系,**优先使用联合类型替代函数重载,除非多个参数之间、或者某个参数与返回值之间,存在对应关系。**相对来说,使用联合类型比使用函数重载会简洁很多

typescript
// 写法一
function len(s:string):number;
function len(arr:any[]):number;
function len(x:any):number {
  return x.length;
}

// 写法二
function len(x:any[]|string):number {
  return x.length;
}

构造函数

JavaScript 语言是使用构造函数,生成对象的实例的。构造函数最大的特点就是,必须使用 new 命令调用

typescript
const d = new Date();

class Animal {
  numLegs:number = 4;
}

type AnimalConstructor = new () => Animal;

function create(c:AnimalConstructor):Animal {
  return new c();
}

const a = create(Animal);

构造函数类型声明(注意:new 命令):

typescript
type F = {
  new (s: string): object;
}

// 如果既可以当做普通函数,又可以当做构造函数,声明写法如下:
type F = {
  new (s: string): object;
  (n?: number): number
}

总结

对象类型

基本使用

typescript
const obj:{
  x:number;
  y:number;
} = { x: 1, y: 1 };

注意⚠️:属性的类型可以用分号结尾,也可以用逗号结尾

typescript
// 属性类型以分号结尾
type MyObj = {
  x: number;
  y: number;
}

// 属性类型以逗号结尾
type MyObj = {
  x: number,
  y: number,
}

// 最后一个属性后面,可以写分号或逗号,也可以不写

对象的方法描述函数:

typescript
const obj:{
  x: number;
  y: number;
  add(x:number, y:number): number;
  // 或者写成
  // add: (x:number, y:number) => number;
} = {
  x: 1,
  y: 1,
  add(x, y) {
    return x + y;
  }
};

对象类型可以使用方括号读取属性的类型:

typescript
type User = {
  name: string,
  age: number
}

type Name = User['name']

interface 命令写法:

typescript
// 写法一
type MyObj = {
  x:number;
  y:number;
};

const obj:MyObj = { x: 1, y: 1 };

// 写法二
interface MyObj {
  x: number;
  y: number;
}

const obj:MyObj = { x: 1, y: 1 };

注意⚠️:TypeScript 不会区分对象自身的属性和继承的属性,一律视为对象的属性

typescript
interface MyInterface {
  toString(): string // 继承的属性
  prop: number // 自身属性
}

const obj: MyInterface = { // 正确
  prop: 123
}

可选属性

typescript
const obj: {
  x: number;
  y?: number;
} = { x: 1 };

**注意⚠️:可选属性等同于允许赋值为 undefined **

typescript
type User = {
  firstName: string;
  lastName?: string;
};

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

const obj: {
  x: number;
  y?: number;
} = { x: 1, y: undefined };

TypeScript 提供编译设置 ExactOptionalPropertyTypes,只要同时打开这个设置和 strictNullChecks,可选属性就不能设为 undefined

typescript
// 打开 ExactOptionsPropertyTypes 和 strictNullChecks
const obj: {
  x: number;
  y?: number;
} = { x: 1, y: undefined }; // 报错

注意⚠️:可选属性与允许设为 undefined 的必选属性是不等价的

typescript
type A = { x:number, y?:number };
type B = { x:number, y:number|undefined };

const ObjA:A = { x: 1 }; // 正确
const ObjB:B = { x: 1 }; // 报错

只读属性

属性名前面加上 readonly 关键字,表示这个属性是只读属性,不能修改 注意⚠️:只读属性只能在对象初始化期间赋值,此后就不能修改属性

typescript
type Point = {
  readonly x: number;
  readonly y: number;
};

const p:Point = { x: 0, y: 0 };

p.x = 100; // 报错

但是⚠️,如果属性是一个对象,readonly 修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象

typescript
interface Home {
  readonly resident: {
    name: string;
    age: number
  };
}

const h:Home = {
  resident: {
    name: 'Vicky',
    age: 42
  }
};

h.resident.age = 32; // 正确
h.resident = {
  name: 'Kate',
  age: 23
} // 报错

高端操作:另一个需要注意的地方是,如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量

typescript
interface Person {
  name: string;
  age: number;
}

interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}

let w:Person = {
  name: 'Vicky',
  age: 42,
};

let r:ReadonlyPerson = w;

w.age += 1;
r.age // 43

注意⚠️:另一种声明只读属性:赋值时,在对象后面加上只读断言 as const,对象后面加上就变成只读对象,不能修改属性

typescript
const myUser = {
  name: 'xxx'
} as const

myUser.name = '1111' // 报错

注意,上面的 as const 属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准

typescript
const myUser:{ name: string } = {
  name: "Sabrina",
} as const;

myUser.name = "Cynthia"; // 正确

属性名的索引类型

使用属性名表达式的写法来描述类型

typescript
type MyObj = {
  // property 表示属性名,这个是可以随便起的
  [property: string]: string
}

const obj: MyObj = {
  foo: 'a',
  bar: 'b',
  baz: 'c'
}

type T1 = {
  [property: number]: string
};

type T2 = {
  [property: symbol]: string
};

type MyArr = {
  [n:number]: number;
};

const arr:MyArr = [1, 2, 3];
// 或者
const arr:MyArr = {
  0: 1,
  1: 2,
  2: 3,
};

对象可以有多种类型索引,比如数值索引和字符串索引。但是数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名 同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名符合属性名索引的范围,两者不能有冲突,否则报错

typescript
type MyType = {
  [x: number]: boolean; // 报错
  [x: string]: string;
}

type MyType = {
  foo: boolean; // 报错
  [x: string]: string;
}

注意⚠️:慎重使用,属性名的声明太宽泛了,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及 length属性,因为类型里面没有定义这些东西

typescript
type MyArr = {
  [n:number]: number;
};

const arr:MyArr = [1, 2, 3];
arr.length // 报错

解构赋值

解构赋值用于直接从对象中提取属性

typescript
const {id, name, price}:{
  id: string;
  name: string;
  price: number
} = product;

// 解构赋值类型的写法,跟为对象声明类型是一样的
const { id, name, price }: {
  id: string;
  name: string;
  price: number
} = product

let { x: foo, y: bar }: { x: string; y: number } = obj;

function draw({
  shape: Shape,
  xPos: number = 100,
  yPos: number = 100
}) {
  let myShape = shape; // 报错
  let x = xPos; // 报错
}

注意⚠️:目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途

结构类型原则

只要对象 B 满足对象 A 的结构特征(跟类型名无关),TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则,简单说 B 包含 A,使用 A 的地方可以使用 B 替代

typescript
type A = {
  x: number;
};

type B = {
  x: number;
  y: number;
};

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

const A:{ x: number } = B; // 正确

如果类型 B 可以赋值给类型 A,TypeScript 就认为 B 是 A 的子类型,A 是 B 的父类型。子类型满足父类型的所有特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型

typescript
type MyObj = {
  x: number,
  y: number,
};

function getSum(obj:MyObj) {
  let sum = 0;

  for (const n of Object.keys(obj)) {
    const v = obj[n]; // 报错,所有兼容的 MyObj 对象都可以使用,可能为 any
    sum += Math.abs(v);
  }

  return sum;
}

// 修改:使用明确的属性值
type MyObj = {
  x: number,
  y: number,
};

function getSum(obj:MyObj) {
  return Math.abs(obj.x) + Math.abs(obj.y);
}

严格字面量检查

如果对象使用字面量表示(直接字面量赋值),会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错

typescript
const point:{
  x:number;
  y:number;
} = {
  x: 1,
  y: 1,
  z: 1 // 报错
};

如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的

typescript
const myPoint = {
  x: 1,
  y: 1,
  z: 1
};

const point:{
  x:number;
  y:number;
} = myPoint; // 正确

TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API

typescript
type Options = {
  title:string;
  darkMode?:boolean;
};

const obj:Options = {
  title: '我的网页',
  darkmode: true, // 报错
};

// 如果为了规避严格字面量检查,可以使用中间变量赋值
let myOptions = {
  title: '我的网页',
  darkmode: true,
};

const obj:Options = myOptions;

// 使用类型断言规避严格字面量检查报错问题
const obj:Options = {
  title: '我的网页',
  darkmode: true,
} as Options;

// 设置允许多余的属性
let x: {
  foo: number,
  [y: string]: any
}

x= { foo: 1, baz: 2 }

// 不能有多余的属性
interface Point {
  x: number;
  y: number;
}

function computeDistance(point: Point) { /*...*/ }

computeDistance({ x: 1, y: 2, z: 3 }); // 报错,编译器选项suppressExcessPropertyErrors,可以关闭多余属性检查
computeDistance({x: 1, y: 2}); // 正确

最小可选属性规则

弱类型检查,防止一个类型对象都是可选属性,任意对象都满足的情况问题

typescript
type Options = {
  a?:number;
  b?:number;
  c?:number;
};

const opts = { d: 123 };

const obj:Options = opts; // 报错

报错原因是,如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则” 如果想规避这条规则,要么在类型里面增加一条索引属性([propName: string]: someType),要么使用类型断言(opts as Options

空对象

空对象是 TypeScript 的一种特殊值,也是一种特殊类型

typescript
const obj = {} // const obj:{} = {};
obj.prop = 123 // 报错
obj.toString() // 正确

// 错误
const pt = {};
pt.x = 3;
pt.y = 4;

// 正确
const pt = {
  x: 3,
  y: 4
};

// 使用分布声明一个对象,使用扩展运算符
const pt0 = {};
const pt1 = { x: 3 };
const pt2 = { y: 4 };

const pt = {
  ...pt0, ...pt1, ...pt2
};

注意⚠️:空对象没有自定义属性,所以对自定义属性赋值就会报错。空对象只能使用继承的属性,即继承自原型对象 Object.prototype 的属性

空对象作为类型

typescript
let d:{};
// 等同于
// let d:Object;

d = {};
d = { x: 1 };
d = 'hello';
d = 2;

各种类型的值(除了null 和 undefined)都可以赋值给空对象类型,跟 Object 类型的行为是一样的 因为 Object 可以接受各种类型的值,而空对象是 Object 类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性

typescript
interface Empty { }
const b:Empty = {myProp: 1, anotherProp: 2}; // 正确
b.myProp // 报错 变量b的类型是空对象,视同Object类型,不会有严格字面量检查,但是读取多余的属性会报错

如果想强制使用没有任何属性的对象,可以采用下面的写法:

typescript
interface WithoutProperties {
  [key: string]: never
}

// 报错
const a: WithoutProperties = { prop: 1 }

[key: string]: never 表示字符串属性名是不存在的,因此其他对象进行赋值时就会报错

总结

Interface 接口

interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构

typescript
interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

方括号运算符可以取出 interface 某个属性的类型

typescript
interface Foo {
  a: string;
}

type A = Foo['a']; // string

表示对象的常见 5 种语法形式:

  • 对象属性
  • 对象的属性索引
  • 对象方法
  • 函数
  • 构造函数
  1. 对象属性:(注意⚠️:属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略)
typescript
interface Point {
  x: number
  y: number
}
  1. 对象的属性索引:
typescript
interface A {
  [prop: string]: number
}

注意:属性索引共用 string、number、symbol 三种类型

一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性

typescript
interface MyObj {
  [prop: string]: number;

  a: boolean;      // 编译错误
}

属性的数值索引,指的是数组的类型

typescript
interface A {
  [prop:number]: string
}

const obj: A = ['a', 'b']

如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名

typescript
interface A {
  [prop: string]: number;
  [prop: number]: string; // 报错,数值索引必须兼容字符串索引的类型声明
}

interface B {
  [prop: string]: number;
  [prop: number]: number; // 正确
}
  1. 对象的方法:
typescript
// 写法一
interface A {
  f(x: boolean): string;
}

// 写法二
interface B {
  f: (x: boolean) => string;
}

// 写法三
interface C {
  f: { (x: boolean): string };
}

或属性名使用表达式:

typescript
const f = 'f';

interface A {
  [f](x: boolean): string;
}

类型方法重载:

typescript
interface A {
  f(): number;
  f(x: boolean): boolean;
  f(x: string, y: string): string;
}

注意:由于没有给出重载的实现方法,需要额外在对象外部给出函数方法的实现

typescript
interface A {
  f(): number;
  f(x: boolean): boolean;
  f(x: string, y: string): string;
}

function MyFunc(): number;
function MyFunc(x: boolean): boolean;
function MyFunc(x: string, y: string): string;
function MyFunc(
  x?:boolean|string, y?:string
):number|boolean|string {
  if (x === undefined && y === undefined) return 1;
  if (typeof x === 'boolean' && y === undefined) return true;
  if (typeof x === 'string' && typeof y === 'string') return 'hello';
  throw new Error('wrong parameters');
}

const a:A = {
  f: MyFunc
}
  1. 函数:声明独立的函数
typescript
interface Add {
  (x: number, y: number): number
}
  1. 构造函数:内部使用 new 关键字,表示构造函数
typescript
interface ErrorConstructor {
  new (message?: string): Error
}

继承

  1. interface 继承 interface,使用 extends 关键字,会从继承的接口里面拷贝属性,这样就不用书写重复属性类型声明
typescript
interface Shape {
  name: string
}

interface Circle extends Shape {
  radius: number
}

interface Style {
  color: string
}

// 多重继承,相当于多个父接口的合并
interface Circle extends Shape, Style {
  radius: number
}

注意⚠️:子接口与父接口存在同名属性情况下,子接口会覆盖父接口的属性。而且,子接口与父接口的同名属性类型必须是兼容,不能有冲突,否则会报错:

typescript
interface Foo {
  id: string;
}

interface Bar extends Foo {
  id: number; // 报错
}

而且,多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错:

typescript
interface Foo {
  id: string;
}

interface Bar {
  id: number;
}

// 报错
interface Baz extends Foo, Bar {
  type: string;
}
  1. interface 继承 type:
typescript
type Country = {
  name: string
  capital: string
}

interface CountryWithPop extends Country {
  population: number
}

注意⚠️:type 声明的是对象才可以继承

  1. interface 继承 class

继承 class 的所有成员属性

typescript
class A {
  x: string = ''

  y (): boolean {
    return true
  }
}

interface B extends A {
  z: number
}

注意⚠️:某些类拥有私有成员和保护成员,interface 可以继承这样的类,但是意义不大

typescript
class A {
  private x: string = '';
  protected y: string = '';
}

interface B extends A {
  z: number
}

// 报错,对象不能实现这些成员
const b:B = { /* ... */ }

// 报错,这个 class 与 A 之间无法构成父子类关系
class C implements B {
  // ...
}

接口合并

多个同名接口会合并成一个接口(注意⚠️:同一个属性有多个类型声明,不能有类型冲突)

typescript
interface Box {
  height: number
  width: number
}

interface Box {
  length: number
}

interface A {
  a: number;
}

interface A {
  a: string; // 报错
}

为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便 使用举例:

typescript
// Web 网页开发经常会对windows对象和document对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义

interface Document {
  foo: string;
}

document.foo = 'hello';

同名接口合并中,同名方法有不同的类型声明,会发生函数重载(注意⚠️:顺序很重要,越靠后定义,优先级越高,排在函数重载的越前面):

typescript
interface Cloner {
  clone(animal: Animal): Animal;
}

interface Cloner {
  clone(animal: Sheep): Sheep;
}

interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}

// 等同于
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
  clone(animal: Sheep): Sheep;
  clone(animal: Animal): Animal;
}

但有一个例外,同名方法中,如果有一个参数是字面量类型,字面量类型有更高的优先级:

typescript
interface A {
  f(x:'foo'): boolean;
}

interface A {
  f(x:any): void;
}

// 等同于
interface A {
  f(x:'foo'): boolean;
  f(x:any): void;
}

// createElement 方法可以根据不同的参数生成不同的 HTML 节点对象
interface Document {
  createElement(tagName: any): Element;
}
interface Document {
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
  createElement(tagName: string): HTMLElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
}

// 等同于,参数为字面量的类型声明会排到最前面,返回具体的 HTML 节点对象。类型越不具体的参数,排在越后面,返回通用的 HTML 节点对象
interface Document {
  createElement(tagName: "canvas"): HTMLCanvasElement;
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
  createElement(tagName: string): HTMLElement;
  createElement(tagName: any): Element;
}

注意:如果使用了联合类型声明,对应的同名属性也是联合类型

typescript
interface Circle {
  area: bigint;
}

interface Rectangle {
  area: number;
}

declare const s: Circle | Rectangle;

s.area;   // bigint | number

interface 与 type 的异同

很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写 type 命令

相同点

  1. 都能为对象类型起名:
typescript
type Country = {
  name: string;
  capital: string;
}

interface Country {
  name: string;
  capital: string;
}

注意:跟 class 定义一个类,同时定义一个对象类型不一样,class 会创造一个值,编译后依然存在,只想要一个类型的话,最好使用 type 或 interface

不同点

  1. type 能够表示非对象类型,而 interface 只能表示对象类型(包括数组、函数等);
  2. interface 可以继承,type 不支持继承(可以使用 & 运算符合并,重新定义一个类型);

注意⚠️:继承时, type 和 interface 可以换用,interface 也可以继承 type(两种方式写法不一样)

typescript
type Foo = { x: number }
interface Bar extends Foo {
  y: number
}

interface Foo = { x: numebr }
type Bar = Foo & { y: number }
  1. 同名 interface 会自动合并,而 type 会报错,type 不允许同名多次定义,说明 interface 是开放的,可以添加属性,type 则是封闭的,不能添加属性,只能定义新的 type;
  2. interface 不能包含属性映射,但是 type 可以:
typescript
interface Point {
  x: number;
  y: number;
}

// 正确
type PointCopy1 = {
  [Key in keyof Point]: Point[Key];
};

// 报错
interface PointCopy2 {
  [Key in keyof Point]: Point[Key];
};
  1. this 关键字只能用于 interface:
typescript
// 正确
interface Foo {
  add(num: number): this;
}

// 报错
type Foo = {
  add(numnumber): this;
}

// 实例
class Calculator implements Foo {
  result = 0

  add (num: number) {
    this.result += num

    return this
  }
}
  1. type 可以扩展原始数据类型,interface 不行:
typescript
// 正确
type MyStr = string & {
  type: 'new'
}

// 报错
interface MyStr extends string {
  type: 'new'
}
  1. interface 无法表达某些复杂类型(比如交叉类型和联合类型),但是 type 可以:
typescript
type A = { /* ... */ };
type B = { /* ... */ };

type AorB = A | B;
type AorBwithName = AorB & {
  name: string
};

综上所述:如果有复杂的类型运算,那么没有其他选择只能使用 type;一般情况下,interface 灵活性比较高,便于扩充类型或自动合并,建议优先使用

总结

Class 类型

基本使用

类的属性可以在顶层声明,也可以在构造方法内部声明

  1. 顶层声明属性:
typescript
class Point {
  x: number;
  y: number;
}

// 不声明,默认是 any
class Point {
  x;
  y;
}

// 声明给初始值,不声明类型,自动推断
class Point {
  x = 0;
  y = 0;
}

// 非空断言
class Point {
  x!: number;
  y!: number;
}

TypeScript strictPropertyInitialization 设置默认初始化值

  1. readonly 只读修饰符:
typescript
class A {
  readonly id = 'foo';
}

const a = new A();
a.id = 'bar'; // 报错

class A {
  readonly id:string;

  constructor() {
    this.id = 'bar'; // 正确,设置只读属性的初始值
  }
}

class A {
  readonly id:string = 'foo';

  constructor() {
    this.id = 'bar'; // 正确,修改只读属性的初始值
  }
}

注意⚠️:构造方法修改只读属性的值也是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错

  1. 方法的类型

类的方法就是普通函数,跟函数的声明方式一致,可以使用参数默认值和函数重载

typescript
class Point {
  x:number;
  y:number;

  constructor(x:number, y:number) {
    this.x = x;
    this.y = y;
  }

  add(point:Point) {
    return new Point(
      this.x + point.x,
      this.y + point.y
    );
  }
}

// 参数默认值
class Point {
  x: number;
  y: number;

  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}

// 函数重载
class Point {
  constructor(x:number, y:string);
  constructor(s:string);
  constructor(xs:number|string, y?:string) {
    // ...
  }
}

注意⚠️:构造方法不能返回值类型,否则报错,因为总是返回实例对象

1.4 存储器方法 存取器是特殊的类方法,包括取值器(读取属性)和存值器(写入属性)

typescript
class C {
  _name = '';
  get name() {
    return this._name;
  }
  set name(value) {
    this._name = value;
  }
}

// 如果只设置 get 则默认只读
class C {
  _name = 'foo';

  get name() {
    return this._name;
  }
}

const c = new C();
c.name = 'bar'; // 报错

get 和 set 方法,TypeScript 5.1 版之前必须类型兼容,之后可以不兼容,但是可访问性必须一致,要么都为公开方法,要么都为私有方法

1.5 属性索引 可以定义属性索引

typescript
class MyClass {
  [s: string]: boolean | ((s: string) => boolean)

  get (s: string) {
    return this[s] as boolean
  }
}

注意⚠️:由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型

typescript
class MyClass {
  [s:string]: boolean;
  f() { // 报错
    return true;
  }
}

// 修改
class MyClass {
  [s:string]: boolean | (() => boolean);
  f() {
    return true;
  }
}

// 属性存储器视同属性
class MyClass {
  [s:string]: boolean;

  get isInstance() { // 不会报错
    return true;
  }
}

类的 interface 接口

  1. implements 关键字

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制

typescript
interface Country {
  name:string;
  capital:string;
}
// 或者
type Country = {
  name:string;
  capital:string;
}

class MyCountry implements Country {
  name = '';
  capital = '';
}

注意⚠️:interface 只是指定检查条件,如果不满足这些条件就会报错,它并不能代替 class 自身的类型声明

typescript
interface A {
  get(name:string): boolean;
}

class B implements A {
  get(s) { // s 的类型是 any,不能替代自身的类型声明
    return true;
  }
}

// 修改
class B implements A {
  get(s:string) {
    return true;
  }
}

interface A {
  x: number;
  y?: number;
}

class B implements A {
  x = 0;
}

const b = new B();
b.y = 10; // 报错

// 修改
class B implements A {
  x = 0;
  y?: number;
}

// 类可以定义接口没有声明的方法和属性
interface Point {
  x: number;
  y: number;
}

class MyPoint implements Point {
  x = 1;
  y = 1;
  z:number = 1;
}

implements 关键字后面,不仅可以是接口,也可以是另一个类,这时后面的类将会被当做接口处理

typescript
class Car {
  id:number = 1;
  move():void {};
}

class MyCar implements Car {
  id = 2; // 不可省略
  move():void {};   // 不可省略
}

注意⚠️:interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法

typescript
interface Foo {
  private member: {} // 报错
}
  1. 实现多个接口

类可以实现多个接口(相当于接受多重限制),每个接口之间使用逗号隔离

typescript
class Car implements MotorVehicle, Flyable, Swimmable {

}

建议:同时实现多个接口并不是一个好的方法,容易使代码难以维护管理,推荐以下方法改造:

  • 类的继承
typescript
class Car implements MotorVehicle {
}

class SecretCar extends Car implements Flyable, Swimmable {
}

// 也可以修改
interface MotorVehicle {
  // ...
}
interface Flyable {
  // ...
}
interface Swimmable {
  // ...
}

interface SuperCar extends MotoVehicle, Flyable, Swimmable {
  // ...
}

class SecretCar implements SuperCar {
  // ...
}
  • 接口的继承
typescript
interface A {
  a: number
}

interface B extends A {
  b: number
}

注意⚠️:发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性

  1. 类与接口的合并

注意⚠️:不允许有两个同名的类,如果有一个类和一个接口同名,那么接口就会被合并到类里面

typescript
class A {
  x:number = 1;
}

interface A {
  y:number;
}

let a = new A();
a.y = 10;

a.x // 1
a.y // 10

注意:合并进类的非空属性,可能为 undefined,没有赋值

typescript
class A {
  x:number = 1;
}

interface A {
  y:number;
}

let a = new A();
a.y // undefined

Class 类型

  1. 实例类型

类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型

typescript
class Color {
  name:string;

  constructor(name:string) {
    this.name = name;
  }
}

const green:Color = new Color('green');

// 其他写法
interface MotorVehicle {
}

class Car implements MotorVehicle {
}

// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();

TypeScript 有三种方法可以为对象类型起名:type、interface 和 class

获取一个类的自身类型:typeof 运算符

typescript
class Point {
  x:number;
  y:number;

  constructor(x:number, y:number) {
    this.x = x;
    this.y = y;
  }
}

// 错误
function createPoint(
  PointClass:Point,
  x: number,
  y: number
) {
  return new PointClass(x, y);
}

// 修改为
function createPoint(
  PointClass:typeof Point,
  x:number,
  y:number
):Point {
  return new PointClass(x, y);
}

JavaScript 中,类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式

typescript
// 如上代码可以修改为
function createPoint(
  PointClass: new (x: number, y: number) => Point,
  // 或者对象形式
  // PointClass: {
    // new (x:number, y:number): Point
  // }
  x: number,
  y: number
): Point {
  return new PointClass(x, y);
}

// 抽离出来
interface PointConstructor {
  new(x:number, y:number):Point;
}

结构类型原则

Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型

typescript
class Foo {
  id!:number;
}

function fn(arg:Foo) {
  // ...
}

const bar = {
  id: 10,
  amount: 100,
};

fn(bar); // 正确

如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合

typescript
class Person {
  name: string;
  age: number;
}

class Customer {
  name: string;
}

// 正确
const cust:Customer = new Person();

总之,只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A 兼容 B 的类型

typescript
// 其他对象跟类的使用结构也是如此
class Person {
  name: string;
}

const obj = { name: 'John' };
const p:Person = obj; // 正确

空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用

typescript
class Empty {}

function fn(x:Empty) {
  // ...
}

fn({});
fn(window);
fn(fn);

注意⚠️:确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法

typescript
class Point {
  x: number;
  y: number;
  static t: number;
  constructor(x:number) {}
}

class Position {
  x: number;
  y: number;
  z: number;
  constructor(x:string) {}
}

const point:Point = new Position('');

如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系

typescript
// 情况一
class A {
  private name = 'a';
}

class B extends A {
}

const a:A = new B();

// 情况二
class A {
  protected name = 'a';
}

class B extends A {
  protected name = 'b';
}

const a:A = new B();

类的继承

类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法 子类可以覆盖基类的同名方法:

typescript
class B extends A {
  greet(name?: string) {
    if (name === undefined) {
      super.greet(); // 注意⚠️:一般使用 super 关键词代替基类的常见做法
    } else {
      console.log(`Hello, ${name}`);
    }
  }
}

// 但是子类的同名方法不能与基类的类型定义冲突
class A {
  greet() {
    console.log('Hello, world!');
  }
}

class B extends A {
  // 报错
  greet(name:string) {
    console.log(`Hello, ${name}`);
  }
}

子类与基类的可访问性设置:

typescript
class A {
  protected x: string = '';
  protected y: string = '';
  protected z: string = '';
}

class B extends A {
  // 正确
  public x:string = '';

  // 正确
  protected y:string = '';

  // 报错
  private z: string = '';
}

注意⚠️:extends 关键词后面不一定是类名,可以是表达式,只要它的类型有构造函数就可以

typescript
// 例一
class MyArray extends Array<number> {}

// 例二
class MyError extends Error {}

// 例三
class A {
  greeting() {
    return 'Hello from A';
  }
}
class B {
  greeting() {
    return 'Hello from B';
  }
}

interface Greeter {
  greeting(): string;
}

interface GreeterConstructor {
  new (): Greeter;
}

function getGreeterBase():GreeterConstructor {
  return Math.random() >= 0.5 ? A : B;
}

class Test extends getGreeterBase() {
  sayHello() {
    console.log(this.greeting());
  }
}

例三,执行之后也是一个表达式

只设置了类型、没有设置初值的顶层属性,有一个比较重要的说明:

typescript
interface Animal {
  animalStuff: any;
}

interface Dog extends Animal {
  dogStuff: any;
}

class AnimalHouse {
  resident: Animal;

  constructor(animal:Animal) {
    this.resident = animal;
  }
}

class DogHouse extends AnimalHouse {
  resident: Dog;

  constructor(dog:Dog) {
    super(dog);
  }
}

// 如果编译代码 target 大于等于 ES2022

const dog = {
  animalStuff: 'animal',
  dogStuff: 'dog'
};

const dogHouse = new DogHouse(dog);

console.log(dogHouse.resident) // 注意这里拿不到基类的值:undefined

// 修改如下:
class DogHouse extends AnimalHouse {
  declare resident: Dog;

  constructor(dog:Dog) {
    super(dog);
  }
}

可访问性修饰符

  • public:默认修饰符,如果省略不写,实际上就带有改修饰符,类的属性和方法默认都是外部可访问的;
  • private:修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员;
  • protected:修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用;

总结

泛型

使用方式:类型名<泛型列表>

1、常见命名规则

惯例:类型参数名称是单个大写字母:用于区分类型变量和普通类或接口名称之间的区别

  • T(Type):类型参数名;
  • K(Key):对象的键类型;
  • V(Value):对象的值类型;
  • P(Property):对象的属性类型;
  • R(Result):类型推导的结果类型;

2、泛型条件

T extends U ? X : Y

ts
// 字符串或数字判断
type IsStr = 'super' extends 'super456' ? true : false
type IsNum = 123 extends 123456 ? true : false

// 对象类型收窄
type IsObj = { status: true, age: 18 } extends { status: true } ? true : false

// Exclude
type Exclude<T, U> = T extends U ? never : T

type T = Exclude<1 | 2, 1 | 3>
// => (1 extends 1 | 3 ? never : 1) | (2 extends 1 | 3 ? never : 2)
// => never | 2
// => 2

3、泛型推断 infer

ts
type FunctionParamType<T> = T extends (...args: infer P) => any ? P : T

实际案例:

ts
interface Person {
  name: string
  age: number
}

type GetAge = (person: Person) => void
const getAge: GetAge = (person) => {}

type Age = FunctionParamType<GetAge> // Person
type TestString = FunctionParamType<string> // string

分布式条件类型

泛型参数与裸类型参数

1、理解分布式条件类型

2、从 TypeScript 源码层面看分布式判断

3、条件类型在工具类型中的重要作用

TypeScript 内置工具类型

  • Partial<T>:将传入属性变成可选;
  • Required<T>:将传入属性变成必选;
  • Readonly<T>:将传入属性变成只读;
  • Record<T, U>:将 T 作为属性,U 作为类型生成新的对象类型;
  • Pick<T, U>:从 T 抽取包含 U 的属性;
  • Omit<T, U>:从 T 删除包含 U 的属性;
  • Exclude<T, U>:从 T 中过滤 U 不存在的属性;
  • Extract<T, U>:从 T 中过滤存在 U 的属性;

函数相关

  • Parameters:函数参数作为元组类型返回;
  • ReturnType:获取函数的返回类型;
  • ConstructorParameters:把构建函数作为一个元素类型返回;

常见 React 类型:

1、TypeScript 内置工具类型的进阶实现

Partial、Required:

  1. 面向实际项目需求的工具类型;
  2. 递归的 Deep 实现;
  3. 更细粒度的部分修饰;

Pick、Omit:

  1. 通过映射类型与索引类型实现接口裁剪;
  2. 更严格的 Omit;
  3. 基于键值类型的接口与裁剪,以及更严格的类型比较;

Exclude 与 Extract:

  1. 又见分布式条件类型;
  2. 交、并、补、差集;
  3. 对象键名的交、并、补、差集;
  4. 类型层面的对象合并;

待更新🚀...

学习资料