TypeScript 学习笔记 
语言介绍 
TypeScript 与 JavaScript 发展史:

TypeScript 与 JavaScript 对比:

类型的概念:类型就是人为添加的一种编程约束和用户提示
运行时做的叫做动态类型检查,运行之前的编译期做的叫做静态类型检查
动态类型检查:源码中不保留类型信息,对某个变量赋什么值、做什么操作都是允许的,写代码很灵活,但有类型不安全隐患,⽐如对 string 做了乘除,对 Date 对象调⽤了 exec ⽅法,这些都是运⾏时才能检查出来的错误
静态类型的优点:
- 有利于代码的静态分析;
- 有利于发现错误;
- IDE 支持;
- 提供代码文档;
- 有利于重构代码;
静态类型的缺点:
- 丧失代码灵活性;
- 增加编程工作量;
- 更高的学习成本;
- 引入独立的编译步骤;
- 兼容性问题;

基础用法 
类型声明 
TypeScript 规定,变量只有赋值后才能使用,否则就会报错
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 使用
// 安装
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.tstsconfig.json 配置
ts-node 模块:便捷方式运行 TypeScript 代码查看结果 ts-node 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码
三种特殊类型 
any 
表示没有任何限制,该类型的变量可以赋予任意类型的值 特点:
- 相当于关闭这个类型检查;
- 顶层类型(top type);
- 污染其他变量(它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错);
使用场景:
- 出于特殊原因,需要关闭某些变量的类型检查;
- 适配老项目,快速迁移;
类型推断为 any 就会报错的配置:
tsc --noImplicitAny app.ts但有个隐患:使用 let 和 var 命令声明变量,但不赋值也不指定类型,是不会报错的
let x;
x = 123;
x = { foo: 'hello' };所以:建议使用 let 和 var 声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患
let x:any = 'hello';
let y:number;
y = x; // 不报错
y * 123 // 不报错
y.toFixed() // 不报错污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用 any 类型的另一个主要原因
unknown 
表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像 any 那样自由,可以视为严格版的 any
凡是需要设置 any 类型的地方都应该优先考虑设为 unknown 类型、除了 any 外其他所有类型的全集
跟 any 相似之处:所有类型的值都可以分配给 unknown 类型、顶层类型
let x:unknown;
x = true; // 正确
x = 42; // 正确
x = 'Hello World'; // 正确跟 any 不同之处:
- 不能直接使用——unknown 类型的变量,不能直接赋值给其他类型的变量(除了 any 类型和 unknown 类型),避免污染变量问题,从而跟 any 类型不一样
let v:unknown = 123;
let v1:boolean = v; // 报错
let v2:number = v; // 报错- 不能直接调用 unknown 类型的变量的方法和属性
let v1:unknown = { foo: 123 };
v1.foo  // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错- unknown 类型能够进行的运算是有限的,只能进行比较运算(运算符:==、===、!=、!==、||、&&、?)、取反运算(运算符:!)、typeof运算符合instanceof运算符这几种
let a:unknown = 1;
a + 1 // 报错
a === 1 // 正确使用 unknown 类型变量的方法:类型缩小——缩小 unknown 变量的类型范围,确保不会出错(明确 unknown 类型变量的实际类型,才允许使用它,防止像 any 一样乱用,变量污染)
let a:unknown = 1;
// 类型缩小
if (typeof a === 'number') {
  let r = a + 10; // 正确
}
if (typeof a === 'string') {
  a.length; // 正确
}never 
不存在任何属于“空类型”的值,这样的类型的成为 never,即不可能有这样的值 特点:
- never 类型的变量可以赋值给任意类型(空集是任何集合的子集);
- 唯一一个底层类型;
使用场景:一些类型运算中,保证类型运算的完整性
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 类型不兼容
const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错
// bigint 赋值给整数和小数都会报错- bigint 类型是 ES2020 标准引入的。如果使用这个类型,TypeScript 编译的目标 JavaScript 版本不能低于 ES2020(即编译参数 target 不低于 es2020)
如果没有声明类型的变量,被赋值为 undefined 或 null,在关闭编译设置 noImplicitAny 和 strictNullChecks 时,它们的类型会被推断为 any
// 关闭 noImplicitAny 和 strictNullChecks
let a = undefined;   // any
const b = undefined; // any
let c = null;        // any
const d = null;      // any包装对象类型 
包装对象:指的是这些类型变量(boolean、string、number)需要时,会自动产生的对象
'hello'.charAt(1) // 'e'包装对象类型与字面量类型
'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 类型的简写形式
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 侠义对象:字面量表是,只包含对象、数组和函数,不包括原始类型的值
let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型 object,不使用大写类型 Object
注意,无论是大写的 Object 类型,还是小写的 object 类型,都只包含 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
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 却没有限制
值类型 
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; // 报错联合类型 
let setting:true|false;
let gender:'male'|'female';
let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';“类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)
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;
  }
}交叉类型 
指的多个类型组成的一个新类型,使用符号 & 表示
let obj:
  { foo: string } &
  { bar: string };
obj = {
  foo: 'hello',
  bar: 'world'
};
type A = { foo: number };
type B = A & { bar: number };type 命令 
type 命令用来定义一个类型的别名 别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型
特点:
- 别名不允许重名;
- 别名的作用域是块级作用域;
- 别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套
- type 命令属于类型相关代码,编译 JavaScript 的时候会全部删除
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 运算符是一个一元运算符,返回一个字符串,代表操作数的类型
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 的参数只能是标识符,不能是需要运算的表达式
type T = typeof Date(); // 报错typeof 命令的参数不能是类型
type Age = number
type MyAge = typeof Age // 报错块级类型声明 
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 的一个规则是,凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行
数组 
两种声明写法
// 字面量定义
const arr: number[] = [1]
// 泛型定义
const arr1: Array<number | string> = [1, '2']
// 注意联合类型的使用,需要搭配括号,跟优先级有关
let arr:(number|string)[];TypeScript 允许使用方括号读取数组成员的类型
type Names = string[];
type Name = Names[0]; // string
// 也可以写成这样
type Name = Names[number]数组类型推断 前提条件是初始值为空的数组 如果没有声明数组类型,数组会根据添加的元素进行类型推断
const arr = [];
arr // 推断为 any[]
arr.push(123);
arr // 推断类型为 number[]
arr.push('abc');
arr // 推断类型为 (string|number)[]
// 注意⚠️:类型不为空的数组
// 推断类型为 number[]
const arr = [123];
arr.push('abc'); // 报错只读数组,const 断言
// 只读数组与数组的父类型关系
function getSum(s:number[]) {
  // ...
}
const arr:readonly number[] = [1, 2, 3];
getSum(arr) // 报错
// 修改
getSum(arr as number[])注意⚠️:readonly 与数组的泛型写法不能一起使用
// 报错
const arr:readonly Array<number> = [0, 1];
// TypeScript 专门的泛型
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];通过使用 const 断言来使用只读数组
const arr = [0, 1] as const
arr[0] = 2 // 报错多维数组
// 数组成员类型为:number
var multi:number[][] =
  [[1,2,3], [23,24,25]];元组 
成员类型写在方括号里面的就是元组,写在外面的就是数组
// 数组
let a:number[] = [1];
// 元组
let t:[number] = [1];元组必须显式给出类型声明 元组成员的类型可以添加问号后缀(?),表示该成员是可选的
let a:[number, number?] = [1];注意⚠️:可选成员必须放在尾部(必须放在必选成员之后)
元组的成员是有限的,越界的成员会报错,但是可以通过扩展符使用不限制成员数量
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];如果不确定元组成员的类型和数量
type Tuple = [...any[]]元组的成员可以添加成员名,这个成员名是说明性的,可以任意取名,没有实际作用
type Color = [
  red: number,
  green: number,
  blue: number
];
const c:Color = [255, 255, 255];元组可以通过方括号,读取成员类型
type Tuple = [string, number];
type Age = Tuple[1]; // number
// 获取所有成员的类型
type Tuple = [string, number, Date];
type TupleEl = Tuple[number];  // string|number|Date只读元组 
// 写法一
type t = readonly [number, string]
// 写法二
type t = Readonly<[number, string]>只读元组不能替代元组
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 会推断出元组的成员数量(即元组长度)
function f(
  point:[number, number?, number?]
) {
  if (point.length === 4) {  // 报错
    // ...
  }
}注意⚠️:使用了扩展符就将无法退出成员的数量(TypeScript 内部会把元组当做数组处理)
const myTuple:[...string[]]
  = ['a', 'b', 'c'];
if (myTuple.length === 4) { // 正确
  // ...
}扩展运算符与成员数量 
扩展运算符(...)将数组(注意,不是元组)转换成一个逗号分隔的序列,这时 TypeScript 会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的
const arr = [1, 2];
function add(x:number, y:number){
  // ...
}
add(...arr) // 报错解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符(补充类型注解)
// 将上面的写法改成这样
const arr:[number, number] = [1, 2];
function add(x:number, y:number){
  // ...
}
add(...arr) // 正确还有一种更简单的写法,使用断言 const (值类型)
const arr = [1, 2] as const既可以当做数组,也可以当做元组使用
Symbol 类型 
Symbol 是 ES2015 新引入的一种原始类型的值。它类似于字符串,但是每一个 Symbol 值都是独一无二的,与其他任何值都不相等
使用
let x:symbol = Symbol();
let y:symbol = Symbol();
x === y // falsesymbol 类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值 unique symbol 表示单个的、某个具体的 Symbol 值,该类型声明的变量不能修改,只能用 const 声明
// 正确
const x:unique symbol = Symbol();
// 报错
let y:unique symbol = Symbol();const 命令为变量赋值 Symbol 值时,变量类型默认就是 unique symbol,所以类型可以省略不写
const x:unique symbol = Symbol();
// 等同于
const x = Symbol();注意⚠️:声明两个都是 unique symbol 的类型变量,值类型都是不相同的,也不能相互赋值
const a:unique symbol = Symbol();
const b:unique symbol = a; // 报错写成相同的类型声明:
const a:unique symbol = Symbol();
const b:typeof a = a; // 正确Symbol.for() 的使用可以创建声明不同类型的但值相等的变量
const a:unique symbol = Symbol.for('foo');
const b:unique symbol = Symbol.for('foo');unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行
const a:unique symbol = Symbol();
const b:symbol = a; // 正确
const c:unique symbol = b; // 报错unique symbol 类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol,不能是 symbol
const x:unique symbol = Symbol();
const y:symbol = Symbol();
interface Foo {
  [x]: string; // 正确
  [y]: string; // 报错
}unique symbol 类型也可以用作类(class)的属性值,但只能赋值给类的 readonly static 属性
class C {
  static readonly foo:unique symbol = Symbol();
}类型推断中有一个注意点:如果 const 变量赋值给另一个 symbol 类型的变量,则推断类型为 symbol,而不是 unique symbol
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 类型;
函数类型 
基本使用 
函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型
function hello(
  txt:string
):void {
  console.log('hello ' + txt);
}注意:返回值的类型通常不用写,TypeScript 默认可以推断出来。但如果防止返回值类型被修改或者为了文档考虑,建议写上
如果一个变量被赋值为一个函数,声明写法有两种:
// 写法一
const hello = function (txt:string) {
  console.log('hello ' + txt);
}
// 写法二
const hello:
  (txt:string) => void
= function (txt) {
  console.log('hello ' + txt);
};函数类型里面的参数名与实际参数名,可以不一致
let f:(x:number) => number;
f = function (y:number) {
  return y;
};函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数
let myFunc:
  (a:number, b:number) => number;
myFunc = (a:number) => a; // 正确
myFunc = (
  a:number, b:number, c:number
) => a + b + c; // 报错如果一个变量要套用另一个函数类型,有一个小技巧,就是使用 typeof 运算符(返回类型),注意:这是一个很有用的技巧,任何需要类型的地方,都可以使用 typeof 运算符从一个值获取类型
function add(
  x:number,
  y:number
) {
  return x + y;
}
const myAdd:typeof add = function (x, y) {
  return x + y;
}函数类型声明还可以采用对象的写法:
let add: {
  (x: number, y: number): number
}
add = function (x, y) {
	return x + y
}
// 写法公式,注意:间隔是冒号
{
  (参数列表): 返回值
}适用场景:声明函数属性类型
let foo: {
  (x:number): void;
  version: string
} = f;Interface 写法,跟对象声明写法一样
interface Myfn {
  (a: number, b: number): number
}
const add: Myfn = (a, b) => a + bFunction 类型 
任何函数都是属于这个类型,相当于 any 收窄为具体的函数类型,Function 类型的值都可以直接执行
function doSomething (f: Function) {
  return f(1, 2, 3)
}注意⚠️:Function 类型的函数可以接受任意数量的参数,每个参数的类型都是 any,返回值的类型也是any,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好
箭头函数 
写法:
const repeat = (str: string, times: number): string => str.repeat(times)注意返回值的声明位置:
type Person = { name: string };
const people = ['alice', 'bob', 'jan'].map(
  (name):Person => ({name})
);可选参数 
如果函数的某个参数可以省略,则在参数名后面加问号表示
function f(x?:number) {
  // ...
}
f(); // OK
f(10); // OK
f(undefined) // OK**参数名带有问号,表示该参数的类型实际上是 原始类型 | undefined,它有可能为undefined,**但是类型设置为 number | undefined 表是要么传入一个数值或 undefined,如果省略就会报错 函数的可选参数只能在参数列表的尾部,跟在必选参数的后面 养成习惯:可选参数永远放在尾部 如果函数参数多个情况下,前面的参数有可能为空,只能显示注明类型包括 undefined,传参时也要传入 undefined
let myFunc:
  (
    a:number|undefined,
    b:number
  ) => number;用到的可选参数记得判断该参数是否为 undefined,防止误用报错(建议设置默认值):
let myFunc:
  (a:number, b?:number) => number;
myFunc = function (x, y) {
  if (y === undefined) {
    return x;
  }
  return x + y;
}参数默认值 
设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值,类型声明可以根据默认值推断出来(注意⚠️:设置了默认值,参数传 undefined,也会触发默认值)
function createPoint(
  x:number = 0,
  y:number = 0
):[number, number] {
  return [x, y];
}
createPoint() // [0, 0]注意⚠️:可选参数跟默认值不能同时使用
function f(x?: number = 0) {} // 报错具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入 undefined
function add(
  x:number = 0,
  y:number
) {
  return x + y;
}
add(1) // 报错
add(undefined, 1) // 正确参数解构 
参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些
type ABC = { a:number; b:number; c:number };
function sum({ a, b, c }:ABC) {
  console.log(a + b + c);
}rest 参数 
rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)
// rest 参数为数组
function joinNumbers(...nums:number[]) {
  // ...
}
// rest 参数为元组
function f(...args:[boolean, number]) {
  // ...
}注意⚠️:元组需要声明每一个剩余参数的类型。如果元组里面的参数是可选的,则要使用可选参数
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 只读参数 
function arraySum(
  arr:readonly number[]
) {
  // ...
  arr[0] = 0; // 报错
}void 类型 
void 类型表示函数没有返回值
function f(): void {
  console.log('xx')
}注意⚠️:允许返回 undefined 或 null
function f1(): void {
  return undefined
}
function f2(): void {
  return null
}如果打开 strictNullChecks 编译选项,那么 void 类型只允许返回 undefined。如果返回 null,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回 undefined
注意⚠️:如果变量、对象方法、函数参数是一个返回值为 void 类型的函数,那么并不代表不能赋值为有返回值的函数。恰恰相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错(原因:这个返回值不重要或者不产生作用)
type voidFunc = () => void;
const f:voidFunc = () => {
  return 123;
};因为这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错
const src = [1, 2, 3];
const ret = [];
src.forEach(el => ret.push(el));注意⚠️:push() 有返回值,表示插入新元素后数组的长度。但是,对于 forEach() 方法来说,这个返回值是没有作用的,根本用不到,所以 TypeScript 不会报错 反例:使用到了这个返回值
type voidFunc = () => void;
const f:voidFunc = () => {
  return 123;
};
f() * 2 // 报错注意⚠️:这种情况仅限于变量、对象方法和函数参数,函数字面量如果声明了返回值是 void 类型,还是不能有返回值
function f():void {
  return true; // 报错
}
const f3 = function ():void {
  return true; // 报错
};如果函数运行结果是抛出错误,也允许返回值写成 void
function throwErr():void {
  throw new Error('something wrong');
}除了函数,其他变量声明为 void 类型没有多大意义(可以作为一个变量类型声明),因为这时只能赋值为 undefined 或者 null(假定没有打开strictNullChecks)
let foo: void = undefined
// 没有打开 strictNullChecks 的情况下
let bar: void = nullnever 类型 
never 类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常的执行结束,never 是唯一一个底层类型,所有其他类型都包括这个类型使用场景:
- 抛出错误的函数:
function fail(msg: string): never {
  throw new Error(msg)
}注意,只有抛出错误,才是 never 类型。如果显式用 return 语句返回一个 Error 对象,返回值就不是 never 类型 另外,由于抛出错误的情况属于 never 类型或 void 类型,所以无法从返回值类型中获知,抛出的是哪一种错误
- 无限执行的函数:
const sing = funciton (): never {
  while(true) {
    ...
  }
}注意⚠️:never 类型不同于 void 类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回 undefined
// 正确
function sing():void {
  console.log('sing');
}
// 报错
function sing():never {
  console.log('sing');
}如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型就是 never。如果程序中调用了一个返回值类型为 never 的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码
function neverReturns():never {
  throw new Error();
}
function f(
  x:string|undefined
) {
  if (x === undefined) {
    neverReturns();
  }
  x; // 推断为 string
}省略 never 类型声明的情况:一个函数在某些情况下是正常返回值的,另一些情况下会抛出错误
function sometimesThrow():number {
  if (Math.random() > 0.5) {
    return 100;
  }
  throw new Error('Something went wrong');
}
const result = sometimesThrow();局部类型 
函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型
function hello(txt:string) {
  type message = string;
  let newTxt:message = 'hello ' + txt;
  return newTxt;
}
const newTxt:message = hello('world'); // 报错高阶函数 
一个函数的返回值是一个函数,那么前一个函数就称为高阶函数(higher-order function)
(someValue: number) => (multiplier: number) => someValue * multiplier;函数重载 
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)
function reverse(str:string):string;
function reverse(arr:any[]):any[];
reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]注意⚠️:到这里并没有结束,后面还必须对函数 reverse() 给予完整的类型声明
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();
}
// 前两行类型声明列举了重载的各种情况。第三行是函数本身的类型声明,它必须与前面已有的重载声明兼容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 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明
function f(x:any):number;
function f(x:string): 0|1;
function f(x:any):any {
  // ...
}
const a:0|1 = f('hi'); // 报错对象的方法也可以实现重载:
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;
  }
}函数重载可以用来精确描述函数参数与返回值之间的关系:
function createElement(
  tag:'a'
):HTMLAnchorElement;
function createElement(
  tag:'canvas'
):HTMLCanvasElement;
function createElement(
  tag:'table'
):HTMLTableElement;
function createElement(
  tag:string
):HTMLElement {
  // ...
}可以通过对象声明表示:
type CreateElement = {
  (tag:'a'): HTMLAnchorElement;
  (tag:'canvas'): HTMLCanvasElement;
  (tag:'table'): HTMLTableElement;
  (tag:string): HTMLElement;
}建议:一般函数重载存在比较复杂的类型声明关系,**优先使用联合类型替代函数重载,除非多个参数之间、或者某个参数与返回值之间,存在对应关系。**相对来说,使用联合类型比使用函数重载会简洁很多
// 写法一
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 命令调用
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 命令):
type F = {
  new (s: string): object;
}
// 如果既可以当做普通函数,又可以当做构造函数,声明写法如下:
type F = {
  new (s: string): object;
  (n?: number): number
}总结 
对象类型 
基本使用 
const obj:{
  x:number;
  y:number;
} = { x: 1, y: 1 };注意⚠️:属性的类型可以用分号结尾,也可以用逗号结尾
// 属性类型以分号结尾
type MyObj = {
  x: number;
  y: number;
}
// 属性类型以逗号结尾
type MyObj = {
  x: number,
  y: number,
}
// 最后一个属性后面,可以写分号或逗号,也可以不写对象的方法描述函数:
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;
  }
};对象类型可以使用方括号读取属性的类型:
type User = {
  name: string,
  age: number
}
type Name = User['name']interface 命令写法:
// 写法一
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 不会区分对象自身的属性和继承的属性,一律视为对象的属性
interface MyInterface {
  toString(): string // 继承的属性
  prop: number // 自身属性
}
const obj: MyInterface = { // 正确
  prop: 123
}可选属性 
const obj: {
  x: number;
  y?: number;
} = { x: 1 };**注意⚠️:可选属性等同于允许赋值为 undefined **
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
// 打开 ExactOptionsPropertyTypes 和 strictNullChecks
const obj: {
  x: number;
  y?: number;
} = { x: 1, y: undefined }; // 报错注意⚠️:可选属性与允许设为 undefined 的必选属性是不等价的
type A = { x:number, y?:number };
type B = { x:number, y:number|undefined };
const ObjA:A = { x: 1 }; // 正确
const ObjB:B = { x: 1 }; // 报错只读属性 
属性名前面加上 readonly 关键字,表示这个属性是只读属性,不能修改 注意⚠️:只读属性只能在对象初始化期间赋值,此后就不能修改属性
type Point = {
  readonly x: number;
  readonly y: number;
};
const p:Point = { x: 0, y: 0 };
p.x = 100; // 报错但是⚠️,如果属性是一个对象,readonly 修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象
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
} // 报错高端操作:另一个需要注意的地方是,如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量
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,对象后面加上就变成只读对象,不能修改属性
const myUser = {
  name: 'xxx'
} as const
myUser.name = '1111' // 报错注意,上面的 as const 属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准
const myUser:{ name: string } = {
  name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 正确属性名的索引类型 
使用属性名表达式的写法来描述类型
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 语言内部,所有的数值属性名都会自动转为字符串属性名 同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名符合属性名索引的范围,两者不能有冲突,否则报错
type MyType = {
  [x: number]: boolean; // 报错
  [x: string]: string;
}
type MyType = {
  foo: boolean; // 报错
  [x: string]: string;
}注意⚠️:慎重使用,属性名的声明太宽泛了,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及 length属性,因为类型里面没有定义这些东西
type MyArr = {
  [n:number]: number;
};
const arr:MyArr = [1, 2, 3];
arr.length // 报错解构赋值 
解构赋值用于直接从对象中提取属性
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 替代
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 的父类型。子类型满足父类型的所有特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型
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)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错
const point:{
  x:number;
  y:number;
} = {
  x: 1,
  y: 1,
  z: 1 // 报错
};如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的
const myPoint = {
  x: 1,
  y: 1,
  z: 1
};
const point:{
  x:number;
  y:number;
} = myPoint; // 正确TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API
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}); // 正确最小可选属性规则 
弱类型检查,防止一个类型对象都是可选属性,任意对象都满足的情况问题
type Options = {
  a?:number;
  b?:number;
  c?:number;
};
const opts = { d: 123 };
const obj:Options = opts; // 报错报错原因是,如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则” 如果想规避这条规则,要么在类型里面增加一条索引属性([propName: string]: someType),要么使用类型断言(opts as Options)
空对象 
空对象是 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 的属性
空对象作为类型
let d:{};
// 等同于
// let d:Object;
d = {};
d = { x: 1 };
d = 'hello';
d = 2;各种类型的值(除了null 和 undefined)都可以赋值给空对象类型,跟 Object 类型的行为是一样的 因为 Object 可以接受各种类型的值,而空对象是 Object 类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性
interface Empty { }
const b:Empty = {myProp: 1, anotherProp: 2}; // 正确
b.myProp // 报错 变量b的类型是空对象,视同Object类型,不会有严格字面量检查,但是读取多余的属性会报错如果想强制使用没有任何属性的对象,可以采用下面的写法:
interface WithoutProperties {
  [key: string]: never
}
// 报错
const a: WithoutProperties = { prop: 1 }[key: string]: never 表示字符串属性名是不存在的,因此其他对象进行赋值时就会报错
总结 
Interface 接口 
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构
interface Person {
  firstName: string;
  lastName: string;
  age: number;
}方括号运算符可以取出 interface 某个属性的类型
interface Foo {
  a: string;
}
type A = Foo['a']; // string表示对象的常见 5 种语法形式:
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
- 对象属性:(注意⚠️:属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略)
interface Point {
  x: number
  y: number
}- 对象的属性索引:
interface A {
  [prop: string]: number
}注意:属性索引共用 string、number、symbol 三种类型
一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性
interface MyObj {
  [prop: string]: number;
  a: boolean;      // 编译错误
}属性的数值索引,指的是数组的类型
interface A {
  [prop:number]: string
}
const obj: A = ['a', 'b']如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名
interface A {
  [prop: string]: number;
  [prop: number]: string; // 报错,数值索引必须兼容字符串索引的类型声明
}
interface B {
  [prop: string]: number;
  [prop: number]: number; // 正确
}- 对象的方法:
// 写法一
interface A {
  f(x: boolean): string;
}
// 写法二
interface B {
  f: (x: boolean) => string;
}
// 写法三
interface C {
  f: { (x: boolean): string };
}或属性名使用表达式:
const f = 'f';
interface A {
  [f](x: boolean): string;
}类型方法重载:
interface A {
  f(): number;
  f(x: boolean): boolean;
  f(x: string, y: string): string;
}注意:由于没有给出重载的实现方法,需要额外在对象外部给出函数方法的实现
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
}- 函数:声明独立的函数
interface Add {
  (x: number, y: number): number
}- 构造函数:内部使用 new 关键字,表示构造函数
interface ErrorConstructor {
  new (message?: string): Error
}继承 
- interface 继承 interface,使用 extends 关键字,会从继承的接口里面拷贝属性,这样就不用书写重复属性类型声明
interface Shape {
  name: string
}
interface Circle extends Shape {
  radius: number
}
interface Style {
  color: string
}
// 多重继承,相当于多个父接口的合并
interface Circle extends Shape, Style {
  radius: number
}注意⚠️:子接口与父接口存在同名属性情况下,子接口会覆盖父接口的属性。而且,子接口与父接口的同名属性类型必须是兼容,不能有冲突,否则会报错:
interface Foo {
  id: string;
}
interface Bar extends Foo {
  id: number; // 报错
}而且,多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错:
interface Foo {
  id: string;
}
interface Bar {
  id: number;
}
// 报错
interface Baz extends Foo, Bar {
  type: string;
}- interface 继承 type:
type Country = {
  name: string
  capital: string
}
interface CountryWithPop extends Country {
  population: number
}注意⚠️:type 声明的是对象才可以继承
- interface 继承 class
继承 class 的所有成员属性
class A {
  x: string = ''
  y (): boolean {
    return true
  }
}
interface B extends A {
  z: number
}注意⚠️:某些类拥有私有成员和保护成员,interface 可以继承这样的类,但是意义不大
class A {
  private x: string = '';
  protected y: string = '';
}
interface B extends A {
  z: number
}
// 报错,对象不能实现这些成员
const b:B = { /* ... */ }
// 报错,这个 class 与 A 之间无法构成父子类关系
class C implements B {
  // ...
}接口合并 
多个同名接口会合并成一个接口(注意⚠️:同一个属性有多个类型声明,不能有类型冲突)
interface Box {
  height: number
  width: number
}
interface Box {
  length: number
}
interface A {
  a: number;
}
interface A {
  a: string; // 报错
}为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便 使用举例:
// Web 网页开发经常会对windows对象和document对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义
interface Document {
  foo: string;
}
document.foo = 'hello';同名接口合并中,同名方法有不同的类型声明,会发生函数重载(注意⚠️:顺序很重要,越靠后定义,优先级越高,排在函数重载的越前面):
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;
}但有一个例外,同名方法中,如果有一个参数是字面量类型,字面量类型有更高的优先级:
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;
}注意:如果使用了联合类型声明,对应的同名属性也是联合类型
interface Circle {
  area: bigint;
}
interface Rectangle {
  area: number;
}
declare const s: Circle | Rectangle;
s.area;   // bigint | numberinterface 与 type 的异同 
很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写 type 命令
相同点 
- 都能为对象类型起名:
type Country = {
  name: string;
  capital: string;
}
interface Country {
  name: string;
  capital: string;
}注意:跟 class 定义一个类,同时定义一个对象类型不一样,class 会创造一个值,编译后依然存在,只想要一个类型的话,最好使用 type 或 interface
不同点 
- type 能够表示非对象类型,而 interface 只能表示对象类型(包括数组、函数等);
- interface 可以继承,type 不支持继承(可以使用 & 运算符合并,重新定义一个类型);
注意⚠️:继承时, type 和 interface 可以换用,interface 也可以继承 type(两种方式写法不一样)
type Foo = { x: number }
interface Bar extends Foo {
  y: number
}
interface Foo = { x: numebr }
type Bar = Foo & { y: number }- 同名 interface 会自动合并,而 type 会报错,type 不允许同名多次定义,说明 interface 是开放的,可以添加属性,type 则是封闭的,不能添加属性,只能定义新的 type;
- interface 不能包含属性映射,但是 type 可以:
interface Point {
  x: number;
  y: number;
}
// 正确
type PointCopy1 = {
  [Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
  [Key in keyof Point]: Point[Key];
};- this 关键字只能用于 interface:
// 正确
interface Foo {
  add(num: number): this;
}
// 报错
type Foo = {
  add(num: number): this;
}
// 实例
class Calculator implements Foo {
  result = 0
  add (num: number) {
    this.result += num
    return this
  }
}- type 可以扩展原始数据类型,interface 不行:
// 正确
type MyStr = string & {
  type: 'new'
}
// 报错
interface MyStr extends string {
  type: 'new'
}- interface 无法表达某些复杂类型(比如交叉类型和联合类型),但是 type 可以:
type A = { /* ... */ };
type B = { /* ... */ };
type AorB = A | B;
type AorBwithName = AorB & {
  name: string
};综上所述:如果有复杂的类型运算,那么没有其他选择只能使用 type;一般情况下,interface 灵活性比较高,便于扩充类型或自动合并,建议优先使用
总结 
Class 类型 
基本使用 
类的属性可以在顶层声明,也可以在构造方法内部声明
- 顶层声明属性:
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 设置默认初始化值
- readonly 只读修饰符:
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'; // 正确,修改只读属性的初始值
  }
}注意⚠️:构造方法修改只读属性的值也是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错
- 方法的类型
类的方法就是普通函数,跟函数的声明方式一致,可以使用参数默认值和函数重载
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 存储器方法 存取器是特殊的类方法,包括取值器(读取属性)和存值器(写入属性)
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 属性索引 可以定义属性索引
class MyClass {
  [s: string]: boolean | ((s: string) => boolean)
  get (s: string) {
    return this[s] as boolean
  }
}注意⚠️:由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型
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 接口 
- implements 关键字
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制
interface Country {
  name:string;
  capital:string;
}
// 或者
type Country = {
  name:string;
  capital:string;
}
class MyCountry implements Country {
  name = '';
  capital = '';
}注意⚠️:interface 只是指定检查条件,如果不满足这些条件就会报错,它并不能代替 class 自身的类型声明
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 关键字后面,不仅可以是接口,也可以是另一个类,这时后面的类将会被当做接口处理
class Car {
  id:number = 1;
  move():void {};
}
class MyCar implements Car {
  id = 2; // 不可省略
  move():void {};   // 不可省略
}注意⚠️:interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法
interface Foo {
  private member: {} // 报错
}- 实现多个接口
类可以实现多个接口(相当于接受多重限制),每个接口之间使用逗号隔离
class Car implements MotorVehicle, Flyable, Swimmable {
}建议:同时实现多个接口并不是一个好的方法,容易使代码难以维护管理,推荐以下方法改造:
- 类的继承
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 {
  // ...
}- 接口的继承
interface A {
  a: number
}
interface B extends A {
  b: number
}注意⚠️:发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性
- 类与接口的合并
注意⚠️:不允许有两个同名的类,如果有一个类和一个接口同名,那么接口就会被合并到类里面
class A {
  x:number = 1;
}
interface A {
  y:number;
}
let a = new A();
a.y = 10;
a.x // 1
a.y // 10注意:合并进类的非空属性,可能为 undefined,没有赋值
class A {
  x:number = 1;
}
interface A {
  y:number;
}
let a = new A();
a.y // undefinedClass 类型 
- 实例类型
类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型
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 运算符
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 中,类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式
// 如上代码可以修改为
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 属于同一个类型
class Foo {
  id!:number;
}
function fn(arg:Foo) {
  // ...
}
const bar = {
  id: 10,
  amount: 100,
};
fn(bar); // 正确如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合
class Person {
  name: string;
  age: number;
}
class Customer {
  name: string;
}
// 正确
const cust:Customer = new Person();总之,只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A 兼容 B 的类型
// 其他对象跟类的使用结构也是如此
class Person {
  name: string;
}
const obj = { name: 'John' };
const p:Person = obj; // 正确空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用
class Empty {}
function fn(x:Empty) {
  // ...
}
fn({});
fn(window);
fn(fn);注意⚠️:确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法
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 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系
// 情况一
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 关键字继承另一个类(这里又称“基类”)的所有属性和方法 子类可以覆盖基类的同名方法:
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}`);
  }
}子类与基类的可访问性设置:
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 关键词后面不一定是类名,可以是表达式,只要它的类型有构造函数就可以
// 例一
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());
  }
}例三,执行之后也是一个表达式
只设置了类型、没有设置初值的顶层属性,有一个比较重要的说明:
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
// 字符串或数字判断
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
// => 23、泛型推断 infer 
type FunctionParamType<T> = T extends (...args: infer P) => any ? P : T实际案例:
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:
- 面向实际项目需求的工具类型;
- 递归的 Deep 实现;
- 更细粒度的部分修饰;
Pick、Omit:
- 通过映射类型与索引类型实现接口裁剪;
- 更严格的 Omit;
- 基于键值类型的接口与裁剪,以及更严格的类型比较;
Exclude 与 Extract:
- 又见分布式条件类型;
- 交、并、补、差集;
- 对象键名的交、并、补、差集;
- 类型层面的对象合并;
待更新🚀...
