TypeScript---从入门到入门

489 阅读53分钟

前言:

TypeScript简单来说是一门JavaScript的升级版本,它的好处是可以在写代码的时候给你一些使用的提示,并且能够防止低级错误的出现在你写的代码中等等!

前提: TypeScript的使用从一个空的文件夹开始,并且浏览器并不认识ts文件,需要将其转换为js文件给浏览器运行(把tsconfig.json文件中的OutDir属性打开并设置为./dist)

一、使用:

1.安装webpack和TypeScript的基本模块:

// 初始化一个package.json文件管理安装的各种包
npm init -y

// 安装webpack以及打包需要使用到的webpack-cli和webpack-dev-server
npm i webpack@4.41.5 webpack-cli@3.3.10 webpack-dev-server -D

// 安装TypeScript以及打包过程中需要使用的插件
npm i typescript ts-node @types/node @types/webpack --save-dev

2.配置webpack.config.ts文件:

要使用Typescript来编写 webpack 配置,需要先安装必要的依赖。

import * as path from 'path';  
import * as webpack from 'webpack';

// 引入 'webpack-dev-server' 防止 TypeScript 报错  
import 'webpack-dev-server';

const config: webpack.Configuration = {  
    mode: 'production', // 模式为生产环境  
    entry: './foo.js', // 入口文件路径  
    output: {  
        path: path.resolve(__dirname, 'dist'), // 输出目录  
        filename: 'foo.bundle.js', // 输出文件名称  
    },  
    devServer:{
        port:"3000"//端口号
        proxy: {}, // 代理
    },
    ... // 以及更多的配置
};

export default config

3.父类型与超类型和子类型:

说明:TypeScript中,父类型会包含子类型的所有成员,也就是说如果A是B的父类型,那么A会兼容B,会导致满足B类型的数据可以直接赋值给A变量,其次,类型会决定取值的范围,如果类型的兼容性同时满足的时候,那就可以赋值了,最后,在TypeScript中使用=,就会存在兼容性的判断,如果没有通过兼容性的判断就会报错,同时,在满足上面的条件下,B就是A的超类型

// c是A的子集
const a: A = c 

// D是b的子集
const b: B = D 

// 常量b可以赋值给a => a是b的父类型
a = b

// 常量a可以赋值给b => 
// b是a的父类型,然后a是b的父类型 <=> a、b的类型相同
b = a

4.注意:

对于ts文件的编译需要注意的是:ts文件不可以直接的去运行

// 假设我编写一个hello.ts的文件
console.log(111)

// 此时我要运行这个文件的时候可以通过下面的命令将其转换为js文件
tsc + `需要编译的文件名` --> tsc hello.ts

// 之后可以通过node来运行这个js文件得到结果
node hello.js

但是这里存在的问题在于每次重新更改文件的时候都需要去重新编译,这样会很麻烦,这样就存在一个编译的优化

// 如果在ts文件中编写一个函数的话,那么在编译之后js中也会存在一个
// 同名函数,这样在ts进行静态检查的时候就会报错
tsc --init

会生成一个ts的配置文件,这个文件生成之后就不会报函数名重复的错误

// 多次修改ts文件需要进行多次ts的编译,显得十分麻烦
tsc --watch

类似监听的效果,当ts文件保存其内容改变之后,它会自动的去编译

// 当ts文件中存在错误时不允许其编译成js文件
tsc -- noEmitOnError --watch
// 如果在ts文件中写了低版本浏览器不兼容的代码,可以通过修改ts配置文件的
// target来决定其编译后可以兼容那些低版本的浏览器运行
"traget": "ES5"

如果你还想得到更加严格的类型检查的话就需要开启严格模式了,严格模式可以提高代码的安全性和可靠性,减少潜在类型错误的处理,在ts的配置文件中严格模式的属性有三个:strict(这是一个综合的选项,结合了多种的严格模式,可以将后面的两种模式理解成它的子集)noImplicitAny(ts默认将没有显式给定类型的变量赋值为any类型,这种模式禁止了这种行为,也就是说所有的变量都需要显式的去给定类型)strictNullChecks(它处理了null和undefined这两个类型,默认情况它们可以赋值给任何类型,但是这个选项表示需要使用联合类型或者是可选属性来处理null和undefined)

二、类型全解:

注意:在ts中,类型的首字母都小写,不需要大写

1.基础类型:

(1)String

// String
let a = 'a';

// String
var b = 'b';

// 缩小取值范围,使其值只能是 'c'
const c = 'c';

// String类型也可以执行字符串的操作比如字符串的拼接、截取等等
let d = a + b + c;

// 缩小取值范围,使其值只能是 'e'
let e: 'e' = 'e';

(2)Number

说明: number 类型包含所有的数字,可以做加减乘除取余等运算,有些定义的方法可以参考 boolean 类型

// number
let a = 1234

// number
let b = a + 1

// number
let c = a - 1

// ... 乘除也是一样,结果都是 number

// boolean
let d = a < b

// 缩小取值范围
const e = 10

// 同样缩小取值范围
let f: 10 = 10

在处理较长的数字的时候,为了可以辨识数字,可以使用一下数字分隔符,数字分隔符并不会影响原始值的大小

let g = 1_000_000 // 等价于1000000
let h: 1_000_000 = 1_000_000 // 等价于1000000
console.log(g === h) // true

(3)Boolean

// 推导a的值是 boolean
let a = true

// 推导b的值是 boolean
var b = true

// 由于 c 使用 const 定义,后面给它赋值为true,因为它的值是不变的,
// 所以 ts 会自动的进行类型的缩小其取值的范围使其值只能是 true
const c = true

// 这种写法和 let boolean = true 是等价的,下面后面的 true 表示给初始值
let d: boolean = true

// 这种写法的效果与 c 类似,都缩小了取值范围,下面这种写法的含义在于它告诉 ts ,
// 这个变量 e 它不是一个普通的 boolean 类型,它是值为 true 的 boolean 类型,
// 从而也把值的范围缩小到为 true
let e: true = true

像 e 这种把类型设置为某个确定的值,它只能在这个类型里面取值的选项是唯一的,这种特性称为类型字面量(仅表示一个值的类型)

(4)null

// null(值只有null一个):
let e: null = null

(5)undefined

// undefined(值只有undefined一个):
let e: undefined = undefined

(6)Array

说明: 一般情况下,数组内存放的内容应该是相同类型,在表示方法上面,一种是T[],一种是Array<T>(这种好像已经废弃),它们作用和功能是一样的,可以根据自己的喜好来

a.基本使用

let a = [1, 2, 3];

let c: string[] = ["1"];

// d是一个只能存放字符串和数字的数组
let d = [1, "1"];

// const声明的数组和对象一样是不会将类型推导的范围缩小的,
// 所以它只能够存放数字和字符串
const e = [2, "2"];

// 数组的初始值是字符串类型,当然这个数组只能存放字符串,
// 所以后面添加布尔类型的时候会报错
let f = ["red"];
f.push("blue");
f.push(true);

// 刚开始是空数组,不知道其数组元素的类型,推导出类型为any,
// 后面通过添加元素开始慢慢整理数组元素的类型,当数组离开定
// 义所在的作用域后,会最终确定一个类型下来
function buildArray() {
  const g = [];
  g.push(1);
  g.push(true);
  return g;
}

let myArray = buildArray();

// 类型“"1"”的参数不能赋给类型“number | boolean”的参数。
myArray.push("1");

b.元组

说明: 是一种特殊的数组,它的长度固定,每个索引的值的类型已知,并且在使用的时候需要显式的注解类型,不然ts遇到方括号就会推导出数组类型

// 像a、b这样数组的类型是这样的: number[]
let a: Array<number> = [1]
let b: number[] = [1]

// 像c这样的元组是这样的: [number, number]
let c: [number, number] = [1, 2]

// 不同类型组合而成的元组
let d: [string, number] = ['zhangsan', 20]

// 元素可选的元组,其中类型末尾的?则表示可选
let e: [number, number?] = [1]

// 元组也支持...运算符(用于收集剩余的元素),
// 可以为元素定义最小的长度(像下面这个例子只
// 有张三这个字符串是必传的参数,剩下的都是可选的)
let f: [string, ...number[]] = ['zhangsan', 1, 2, 3]

批量生产(举例): 如果一个项目中使用过多的元组的话,你有不想自己使用类型断言写的话,就可以写一个函数帮你实现效果

function tuple<T extends unknown[]>(...arg: T): T {
    return arg
}

// [number, boolean]
let a = tuple(1, true)
  • 参数:参数是用...剩余参数表示,那么参数的数量是一定的,并且将传入的参数合并为一个数组
  • 参数类型:T继承unknown[],这表示T可以是任意类型的数组,加上前面参数的数量是一定的,那T就是一个元组类型了
  • 返回值:将传入的参数被收集成数组返回
  • 返回值类型:将推断出来的元组T的类型返回

c.只读数组(元组)

说明: 使用readonly关键字使数组的元素是不可以更改的,只能够去去读,如果需要使用对只读数组使用方法,那么使用的方法必须是无法修改原始数组的,否则就会报错,阻止你的修改,

// 常用只读数组定义的四种方式
let g: readonly number[] = [1, 2, 3]
let g: ReadonlyArray<number> = [1, 2, 3]
type FirstOnlyArray = readonly number[]
type SecondOnlyArray = Readonly<number[]>

// 注意:使用类型别名的时候其类型是推导不出来的,
// 可以像下面类型别名无法检测那样使用一下satisfies关键字
let j = [1] satisfies SecondOnlyArray 

// [1, 2, 3, 4]
let h = g.concat(4)

// 只读数组上不存在属性push
let i = g.push(1)

(7)Function

a.常用形式

// 具名函数
function greet(name: string) {
    return 'Hello ' + name
}

// 函数表达式
let greet2 = function(name: string) {
    return 'Hello ' + name
}

// 箭头函数
let greet3 = (name: string) => {
    return 'Hello ' + name
}

// 构造函数:不推荐,当时将鼠标移入到 greet4 上面,
// 它的类型是一个 Function,这是一种可调用的对象,
// 并具有 Function.prototype 所有的原型方法,
// 但其没有体现参数和返回值的类型,因此可以用任
// 何参数调用函数,导致ts会看你做一下不合理的事
// 情但它不警告你
let greet4 = new Function('name')

b.调用签名

说明: 在我理解,每次定义函数都最好去显式注视参数和返回值的类型,但是如果存在很多这样参数类型相同、返回值类型相同的函数,每次都去写就显得很冗余了,这时候就可以把相同的部分提取出来,将其写成(a: T, b?: T) => T这种格式,这种形式的表达式叫做调用签名,也叫类型签名,它只包含类型层面的代码(只有类型和类型运算符),但它没有函数的定义体,无法推断返回值的类型,所以必须显式的注释

// 定义签名 Log(简写形式)
type Log = (message: string, userId?: string) => void

// 定义签名 Log(完整形式)
type Log = {
    (message: string, userId?: string): void
}

// 函数log使用签名Log
let log: Log = (message, userId = '1') => {}

// 函数log1使用签名Log
let log1: Log = (message, userId = '2') => {}

完整的函数签名也可以用来给函数添加属性

type WarnUser = {
    (warning: string): void
    wasCalled: boolean
}

// 这里必须使用 const,使用 let 和 var 都会报错
const warnUser: WarnUser = (warning: string) => {
    if(warnUser.wasCalled) {
        return
    }
    warnUser.wasCalled = true
    alert(warning)
}
warnUser.wasCalled = false

c.函数重载

说明: 在js里面,大多数时候返回值的类型是和参数的类型是有关联的,并且类型都是不确定的,一般情况下,ts中的参数和返回值的类型必须是确定的,不然有点类似any的感觉,会失去类型的提示,为了解决这个问题,ts里面存在函数重载来帮助我们解决这个问题,在我看来,重载就是把函数参数取值的类型多种不确定将其细化成参数类型是某一种,返回值是某一种,将其一一列举出来,这样函数在运行的时候函数参数的类型和返回值的类型都是确定了,然后去写函数逻辑就可以了,总结起来就是:参数千万条,逻辑只一条,类型不确定,执行两行泪

// 写法一:
// 举例:简单实现一下jQuery中的attr的方法
// 传入两个参数获取节点的属性名,传入三个参数获取节点的属性名和属性值
function attr(dom: HTMLElement, attrName: string): string
function attr(dom: HTMLElement, attrName: string, attrValue: string): void
function attr(dom: HTMLElement, attrName: string, attrValue?: string): string | void {
    if(attrValue) {
        dom.setAttribute(attrName, attrValue)
    } else {
        return dom.getAttribute(attrName)
    }
}

重载函数之间写的时候之间不能有其他内容或者是空行的

// 写法二:

type Reserve = {
    (from: Date, to: Date, destination: string): void
    (from: Date, destination: string): void
    (from: Date, toOrDestination: Date | string, destination: string): void
}

let reservel: Reserve = (from: Date, toOrDestination: Date | string, destination?: string) => {
    // 重载的主体逻辑
} 

重载签名的类型越精确越好,因为如果类型存在多种会让你多写很多区分类型的代码,这是为了不必要的麻烦以及保证代码的健壮性

函数的兼容性(参数和返回值):如果两个函数的参数相同的情况下,函数的兼容性等价于函数返回值类型的兼容性,如果两个函数的返回值的类型相同的情况下,函数的兼容性等价于参数对象兼容性的取反的情况

2.特殊类型:

顶部类型(是所有类型的父类型):anyunknow
底部类型(是所有类型的子类型):undefinednull(非严格模式,never除外)never(主要以函数返回值的形式)any

(1)void

说明: 一个函数没有显式的返回值的时候其返回值的类型是void,就像只使用console.log()一样

function echo(msg: string): void {}

(2)never

说明: 一个函数根本没有返回值的时候其返回值的类型是never,比如函数抛出错误函数一直在执行,没有停止这种情况

// 无限循环会出现never类型
function loop(): never {
    while (1) {
        console.log(111)
    }
} 

// 函数无法正常返回或者抛出错误会出现never类型
function error() {
    throw new Error("出现错误")
} 

// 推断的值的类型是不可能出现的类型会出现never类型
function getResult(value: string | number): string {
  if (typeof value === "boolean") {
    // value 类型被推断为 `never`
    return "boolean value is not acceptable";
  }
} 

(3)对象

a.object

说明: 它是 TypeScript 中的一种类型,它表示是一个非原始类型的对象,但并不确定对象包含哪些属性,也不限定属性的名称和类型。因此,object 类型可以包括普通对象、数组、函数、类实例等非原始类型,其次,由于它没有具体属性定义的对象类型,那么它们无法在编译时进行属性检查和代码提示。因此,在访问对象的属性时会出现编译错误,所以这种类型的对象是只能够定义属性或者是方法,但是不能够使用属性或者是方法的

let obj: object = {
  name: 'Alice',
  age: 25,
}


console.log(obj.name); // 类型 obj 上面不存在属性 name
console.log(obj.age); // 类型 obj 上面不存在属性 age
let o: object = [1,2,3]; // object表示除简单数据类型以外的类型(一般创建引用类型)。
let o1: object = new Data() // 可以使用new关键字来创建

b.Object

说明: Object 是 JavaScript 内置的对象类型,它表示 JavaScript 中的对象,包括普通对象、函数、数组、Date、RegExp 等等。在 TypeScript 中使用 Object 类型时,它包含了 object 类型的所有特性,但同时也包含了 JavaScript 内置对象的一些特性,因此它可以访问 JavaScript 内置对象的方法和属性,但可能会导致类型不够具体,无法进行一些类型检查,当然,这种类型的变量也是只能够定义属性或者是方法无法读取的

let obj: Object = {
  name: 'Alice',
  age: 25,
}


console.log(obj.name); // 类型 obj 上面不存在属性 name
console.log(obj.age); // 类型 obj 上面不存在属性 age

c.对象字面量

说明: 可以理解为定义一个对象的结构,后面必须满足这个结构才可以

// 如果两边有一方不满足条件就会报错
let c: {
  a: 1
} = {
  a: 1,
}

{}类型表示一个没任何属性的对象,当你尝试给它赋值便会报错,除了nullundefined之外的所有类型都可以给它赋值

let n: {} = {} <=> let n = {} 

d.属性的限制

let a: {
  // 使用?表示这个属性是一个可选的,表示对象 a 可以没有这个属性
  b?: number;
  // readonly 表示这个属性是一个只读的属性,如果想要修改会报错的
  readonly c: number;
  // [key: T]: U 称为索引签名,表示在对象里面,类型为 T 的键对应的值为 U 类型,
  //           那也就表示这个对象可以添加更多这种符合格式的间值对,需要注意的点
  //           在于 T 必须可以赋值给 number 或者是 string,同时在写法上面一般是
  //           使用 key 的,也可以根据自己的喜好而定 
  [key: number]: boolean
} = {
  c: 1,
  // 额外的属性如果不是给定的 number 或者是 string 类型的话会报错
  '1': true,
  2: true
}

// 这里会提示 无法为“c”赋值,因为它是只读属性
a.c = 2

{}、object、Object类型都不是null和undefined类型的父类型

在对象类型中,如果处于兼容的情况下,子类型必须包含父类型的所有类型(也就是父类型的属性肯定比子类型少,那么空对象肯定是所有对象的父类型)

(4)any

a.基础使用

说明: 这是一个底部类型,同时也是一个顶部类型,那也就是any类型的值跟普通的javaScript的值是一样的,也就会失去类型检查,所以除非有必须使用的地方,否则不要去使用它,如果非要使用,必须显式注解,不要让类型检查将其检测为any类型,不然可能会失去类型提示导致程序问题

let a: any = 666;

let b: any = ["111", 1111];

// 使用 any 类型的话需要显式注解
let c: any = a + b;

b.特殊状况

如果没有明确指定变量的类型,并且编辑器无法从初始值推断出类型,则ts默认会将其推断成any类型

// 这是因为 TypeScript 中的 null 和 undefined
// 被认为是任何类型的子类型,因此它们可以被赋
// 值给任何类型的变量。因此,当一个变量被赋值
// 为 null 时,它的类型会被认为是 any,因为它
// 可以代表任何类型的值。
let a = null
let b = undefined

(5)unknown

说明: 表示值得类型不可知或者未定义跟any类型,都是顶部类型,只不过 TypeScript没有对其放弃类型检查,使用起来是安全的,但是unknown类型在没有被转变为确定类型之前是不能赋值给除它和any以外的类型的

// a是一个不确定的类型,初始值为30
let a: unknown = 30;

// unknown类型是可以比较的
let b = a === 123;

// 在不确定类型之前 unknow 类型是不能做操作的
let c = a + 10;

// unknow 类型是可以用 typeof instantof 操作符细化类型的
if (typeof a === "number") {
  const d = a + 10;
}

// unknown类型是可以否定的
let e = !a

3.穷尽性检查:

说明: 在TypeScript 中,当使用联合类型或枚举类型进行条件判断时,编译器会进行穷尽性检查,确保对所有可能的情况进行了处理,以避免遗漏或未处理的情况。

type Color = "Red" | "Blue" | "Green";

function getColorName(color: Color): string {
  switch (color) {
    case "Red":
      return "红色";
    case "Blue":
      return "蓝色";
  }// 没有处理 "Green" 的情况,会触发穷尽性检查错误
}

// 编译报错: "Function lacks ending return statement and return type does not include 'undefined'"

通过对所有情况进行穷尽性检查,可以避免遗漏或未处理的情况,增强代码的安全性和可靠性。当使用联合类型或枚举类型进行条件判断时,务必确保对所有可能的类型进行了处理。

4.接口

说明:类型别名相似,它们属于同一概念的两种句法,就像函数表达式和函数声明的关系一样

(1)与类型别名的联系

type Sushi = {
  calories: number;
  salty: boolean;
  tasty: boolean;
};

interface Sushi {
  calories: number;
  salty: boolean;
  tasty: boolean;
}

在使用Sushi类型别名的地方都能够使用接口Sushi,两个声明都定义结构,其实二者是等价的

type Food = {
  calories: number;
  salty: boolean;
};

type Sushi = Food & {
  salty: boolean;
};

interface Food {
  calories: number;
  salty: boolean;
}

interface Sushi extends Food {
  salty: boolean;
}

当然,接口跟类型别名一样可以扩展,只不过前者使用extends关键字,后者使用交集运算符

type A = number;

从适用范围来看,类型别名更加通用,因为右边可以是任何类型,包括类型表达式,但是在接口里面,右边必须是结构才行,上面的类型别名A就不能够使用接口重写

interface B {
  good(): string;
}

interface C extends B {
  good(): boolean;
}

使用接口的好处在于如果你想要扩展接口的时候,它会尽可能的去检测扩展的接口是否可赋值给被扩展的接口,确保操作的严谨和正确性

(2)声明合并

interface User {
  name: string;
}

interface User {
  age: number;
}

let a: User = {
  name: "zhangsan",
  age: 50,
};
type User = {
  // 标识符User重复
  name: string;
};

type User = {
  // 标识符User重复
  age: number;
};

同一作用域中的多个同名的接口会自动合并,多个同名的类型别名会导致编译错误

interface User<Age extends number> {
  // User的所有声明都必须具有相同的类型参数
  age: Age
}

interface User<Age extends string> {
  // User的所有声明都必须具有相同的类型参数
  age: Age
}

如果接口声明了泛型,那么两个接口的泛型需要用一模一样的方式来声明,这样才可以合并接口

5.条件类型

说明: 这与值使用的三元表达式类似,它也能嵌套使用,只不过这里使用在类型上使用

type IsString<T> = T extends string ? true : false

// false
type a = IsString<number>

// true
type b = IsString<string>

(1)条件分配

举例:

type ToArray<T> = T[]
type ToArray2<T> = T extends unknown ? T[] : T[]

// number[]
type a = ToArray<number>
// (string | number)[]
type b = ToArray<number | string>

// number[]
type c = ToArray2<number>
// number[] | string[]
type d = ToArray2<number | string> 

看上去ToArray<T>ToArray2<T>没什么区别,都会将传入的泛型变成元组,但是在使用的时候就会发现bd虽然传入的泛型是一致的,但是得到的结果不一样,因为在使用条件类型的时候,ts会将并集类型分配到各个条件分支上,可以直接理解成它会将并集类型的每一部分都会放到条件分支上面去判断,以此得到最终的结果

加深印象: 写一个类型用于计算在T而不在U的类型

type Without<T, U> = T extends U ? never : T;

type A = number | string | boolean;
type B = number;

// string | boolean
type C = Without<A, B>;

过程分析:

// 首先会解析传进来的类型
type C = Without<number | string | boolean, number>;

// 然后把条件分配到并集里面
type C =
  | Without<boolean, boolean>
  | Without<string, boolean>
  | Without<number, boolean>;

// 带入Without替换T和U
type C =
  | (boolean extends boolean ? never : boolean)
  | (string extends boolean ? never : string)
  | (number extends boolean ? never : number);

// 进行计算得到结果
type C = never | string | number

// 化简
type C = string | boolean

(2)infer

说明: 在条件类型中,从一个复杂的类型推断中提取出一部分具体的类型信息

举个栗子: 获取数组中元素的类型

type ElementType<T> = T extends unknown[] ? T[number] : T

// string | number
type a = ElementType<[number, string]>

T[number]:可以这样理解,T表示数组,number表示数字,在数组中那就是索引了,那T[number]就是表示数组内所有的元素了,也就是元素的所有类型了

用infer改写:

type ElementType2<T> = T extends (infer U)[] ? U : T

// number
type b = ElementType2<number[]>

T是数组的时候,就会得到(infer U)[]也是数组,而数组的展示形式为T[],如果是一个number[],那就得到infer u是一个number类型了,由于infer只是起推断的作用,因此U的类型就是number

三、类型延伸

1.显式类型:

说明: 这个一般用于函数的参数、返回值、类、接口的定义中使用的比较多,用于指定当前的变量名应该是什么类型...

// 这里表示函数great在执行的时候需要一个字符串的变量,并且返回值
// 也是需要一个字符串才不会报错,如果有那个地方不满足条件就会报错
function great(person: string): string {}

2.类型推导:

说明: 这个一般在变量的声明这里体现的比较明显。

// 这里给a变量一个显式的number类型
let a: number = 1

// 其实这里并不需要显式的去给定,因为在你给这个变量赋值的时候ts会
// 进行类型的推断,将推断出来的类型去给这个变量

let a = 1  // 这里等号右边是一个number的类型,ts会将这个类型赋值给a变量

a = 'string' // 这里就会报错,因为上面给定变量a的类型是number,这里赋值
             // 赋值确实一个string的类型,报错是必然的

其中,像:number这样的类型声明称为类型注释

3.类型拓宽:

(1)基本使用

// 声明变量的时候如果允许以后修改变量的值(使用let和var声明),
// 变量的类型会从字面值放大到该字面量的基类型
let a = "1"; // string
// 如果声明不可变的常量时,它的类型只是该字面量
const b = "1"; // '1'
// 声明的变量,可以使用显式注解类型防止其类型拓宽
let c: "1" = "1"; // '1'
// 如果将一个定义的常量赋值给一个变量,那么变量的类型会自动拓宽,
// 如果不想这样的话,那么就一开始需要显式注解类型
const d = 1; // 1
let e = d; // number

const f: 1 = 1; // 1
let g = f; // 1
// null和undefined初始化会拓宽成any,但是如果它离开它声明所在的作用域后,
// ts会为其分配一个具体的类型
let h = null; // any
let i = null; // any

function j() {
  let k = null; // any
  k = 3; // any
  return k;
}

j(); // number

4.细化:

说明: 对变量的类型进行判断以此来缩小变量的类型范围,一般会与条件语句一起使用,

注意:类型细化的能力是有限的,只能细化当前作用域中变量的类型,一旦离开这个作用域,细化的能力并不会转移到新的作用域中

(1)基本使用

举个栗子: 假设我需要将css样式中的值和单位解析出来(比如宽度)

分析: 但这样我得到的值可能是一个带单位的字符串,或者一个数字(它没单位的话可以默认给一个),或者是一个连单位都没有的字符串(比如颜色什么的,这样就可以返回一个null),或者什么也没有(比如传入进来的是null或者undefined)

// 用并集表示可能使用到的单位
type Unit = 'px' | 'rem' | '%'

// 假设宽度的格式定位这样
type Width = {
  unit: Unit,
  value: number
}

// 列举单位(用数组方便使用数据)
let units: Unit[] = ['%', 'px', 'rem']

function parseWidth(width: string | number | null | undefined): Width | null {
  // 排出传入的值是undefined和null的情况(ts在不做等值检查的时候null和undefined的结果是一样的)
  if(width == null) {
    return null
  }

  // 这是为数字的时候,这里默认给的单位是'px'
  if(typeof width === "number") {
    return {
      unit: 'px',
      value: width
    }
  }

  // 现在就只剩下解析字符串得到单位了
  let unit = parseUnit(width)

  // 由于得到的单位可能不存在也就是函数的返回值是null的情况,这种情况需要排出下
  if(unit) {
    return {
      unit,
      // parseFloat: 从头开始解析字符串得到里面的数字,直到非数字为止
      value: parseFloat(width)
    }
  }

  // 最后就是没有解析到单位的情况了,那就返回null
  return null
}

// 检查单位,如果没有匹配就返回null
function parseUnit(value: string): Unit | null {
  for(let i = 0; i < units.length; i++) {
    // endsWith:检查字符串是否以指定的子字符串结尾。它返回一个布尔值
    if(value.endsWith(units[i])) {
      return units[i]
    }
  }
  return null
}

(2)并集类型

说明: 看一个复杂一点的例子,假设我需要一个函数处理用户传递的一些事件

简单:

// 假设这是一个input事件,value表示用于输入的值
type UserTextEvent = {
  value: string
}

// 假设这是一个鼠标事件,value表示鼠标移动到的左边的位置
type UserMouseEvent = {
  value: [number, number]
}

// 现在用户就使用了这两个事件
type UserEvent = UserTextEvent | UserMouseEvent

// 现在进行事件的细化
function handle(event: UserEvent) {
  if(typeof event.value === 'string') {
    // string
    event.value
    return
  }
  // [number, number]
  event.value
}

复杂:

type UserTextEvent = {
  type: 'TextEvent',
  value: string;
  target: HTMLInputElement;
};

type UserMouseEvent = {
  type: 'MouseEvent',
  value: [number, number];
  target: HTMLElement;
};

type UserEvent = UserTextEvent | UserMouseEvent;

// handle函数接受的参数类型可能是UserTextEvent、UserMouseEvent、
// UserTextEvent | UserMouseEvent这三种之一,这样event.value
// 和event.target的类型也有多种情况,下面只分出value的情况,但是
// 在value的值确定的时候下面这样是很难区分开的,这里就要使用标记了
function handle(event: UserEvent) {
  if (event.type === "TextEvent") {
    // string
    event.value;
    // HTMLInputElement
    event.target;
    return;
  }
  // [number, number]
  event.value;
  // HTMLElement
  event.target
}

使用标记需要注意:

  • 标记需要放在相同位置的相同部分上面,如果是对象类型,使用相同的字段,如果是元组的类型,使用相同的索引
  • 标记的值使用字面量类型(比如数字、字符串、布尔值啥的),可以混用,但是最好使用同一种类型
  • 不要使用泛型,
  • 标记需要是独一无二的存在

(3)自定义类型防护措施

说明: 如果函数细化了参数的类型,而且返回一个布尔值,就可以使用自定义类型保护措施来使细化的能力在作用域之间转移,在使用该函数的任何地方都能够生效,is运算符就可以完成这样的效果

举例:

// 这里表示如果函数的返回值为true的时候a表示的是字符串类型,
// 如果这里函数返回的类型是boolean的话下面函数的写法就会报错了
function isString(a: unknown): a is string {
    return typeof a === 'string'
}

function parseInput(input: number | string): void {
    let formattedInput: string
    if(isString(input)) {
        formattedInput = input.toUpperCase()
    }
}

注意:自定义类型防护措施只接受一个参数,但参数的类型可以不是简单类型

5.全面性检查:

说明: 这是检查你的代码是否存在遗漏的情况,比如像下面这样的

type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri'
type Day = Weekday | 'Sat' | 'Sun'

function getNextDay(w: Weekday): Day |  {
    // 这个只返回值为'Mon'的情况,如果没有就没有返回值了
    switch (w) {
        case 'Mon':
            return 'Thu'
    }
}

noImplicitReturns: 检查函数的所有代码路径都会有返回值

6.型变

说明: 我的理解是用于判断复杂类型中的父子类型

(1)协变

举个栗子:

// 假设这是一个用户的数据
type ExistingUser = {
  id: number;
  name: string;
};

// 用于删除用户的id
function deleteUser(user: { id?: number; name: string }) {
  delete user.id;
}

let existingUser: ExistingUser = {
  id: 123,
  name: "zhangsan",
};

deleteUser(existingUser);

当你在函数deleteUser的参数类型声明中使用可选属性id?: number时,TypeScript不会报错是因为可选属性的存在意味着属性可以存在也可以不存在。因此,尽管函数内部尝试删除user.id,但由于属性是可选的,TypeScript不会产生错误,所以在某个类型的超类型的地方使用该类型是不安全的,但是不会报错

再来一个:

// 用于删除用户的id
function deleteUser(user: { id?: number; name: string }) {
  delete user.id;
}

type LegacyUser = {
  id?: number | string;
  name: string;
};

let legacyUser: LegacyUser = {
  id: 1,
  name: "zhangsan",
};

// string | number | undefined”不能分配给类型“number | undefined”。  
deleteUser(legacyUser);

所以在某个类型的子类型的地方使用该类型也是不安全的,同时也会报错

总结:

  • 对于预期的结构,使用属性的类型结构需要是预期类型结构的子类型,虽存在安全问题但不报错,切记不能为超类型,因为会直接报错,在类型上,这种行为称作ts对结构(对象和类)的属性类型进行了协变
  • 这样就得到A对象可赋值给B对象的话,A对象所有的属性必须是B对象对应属性的子类型

(2)逆变

结论:

函数a是函数b的子类型条件:

  • 函数a的参数数量≤函数b的参数数量
  • 函数a的this类型未指定,或者是函数b的this的类型的超类型
  • 函数a的各个参数的类型是函数b对应参数的超类型
  • 函数a的返回值的类型是函数b的返回值类型的子类型

四、类型转化

1.类型别名(type):

说明: 就是给类型起一个额外的名称或者一个新的类型名称

(1)基本使用

注意:一般重新起的名字首字母大写

type Name = string // 这里将string这个类型给一个新名字Name

const a: Name = ''
const b: string = ''

console.log(a === b) // Name(虽然a和b长相不同,但是其本质都是string)

类型别名可以理解成给类型定义成一个常量,这样在项目之中存在很多复杂的类型的时候,通过别名就可以大大简化代码,提高其可读性,达到便于维护的效果

(2)类型别名无法检测

type Age = number;

// 这里Person里面的age的类型是Age,但是使用age的时候还是
// 会检测成number类型的 
type Person = {
  name: string;
  age: Age;
};

let age: Age = 55

// 这里person的类型是Person,而不是Person内具体的结构
let person: Person = {
  name: 'Alice',
  age,
};

// 但是使用的时候还是会推测出其实number类型的
let a = person.age
// satisfies:表达式a的类型满足表达式b类型的一种情况

// 上面那样写没有类型提示,这样写就有了
let person = {
  name: "Alice",
  age,
} satisfies Person;

ts的类型检测无法推导类型别名,所以使用的时候需要注意一下

(3)作用域

说明: 类型别名采用的是块级作用域,每一块代码每一个函数都存在自己的作用域,内部的类型别名会覆盖外面的类型别名,当然,在同一作用域下也是不允许重复命名的

type Color = "red";

let X = Math.random() < 0.5;

if (X) {
  // Shadowed name: 'Color',在严格模式下会出现名称屏蔽
  //                 的问题,但不会导致程序 运行错误
  type Color = "blue";
  
  // 这里的Color类型别修改成了blue了
  const a: Color = "blue";
}

2.联合类型(|):

说明: 联合类型可以理解为集合中的并集,如果类型1类型2组合成类型3,那么类型3可以只满足类型1的条件,也可以只满足类型2的条件,也可以类型1类型2的条件都满足

(1)简单理解

type Cat = {
  name: string;
  purrs: boolean;
};

type Dog = {
  name: string;
  barks: boolean;
  wags: boolean;
};

// 像这样的联合类型只有下面的几种情况
type CatOrDogOrBoth = Cat | Dog;

// 只满足类型Cat
let a: CatOrDogOrBoth = {
  name: "1",
  purrs: true
};

// 只满足类型Dog
let b: CatOrDogOrBoth = {
  name: "1",
  barks: true,
  wags: true,
};

// 既满足类型Dog,又满足类型Cat
let c: CatOrDogOrBoth = {
  name: "1",
  barks: true,
  purrs: true,
  wags: true,
};

3.交叉类型(&):

说明: 交叉类型类型就是将子类型进行结合产生一种新的类型,有一点取交集的意味,如果没有交集的话就会出现never类型

type sn = number & 3 // 这里交叉的话会sn表示的是类型3

type sn = string & number // number和string类型并没有交集,得到never类型

type sn = { name: string } & { age: number }
type sn = { name: string, age: number } // 上面的类型交叉的话等效产生这个对象类型

type sn = { name: string } & { name: number, age: number }
type sn = { name: never, age: number } // 如果属性名出现交叉的情况那么会先将属性名进行交叉,再将类型进行交叉

数组能够产生联合类型(举例)

type t = [number, string, boolean]
let arr: t = [1, "2", true] // 这里定义了一个元组

// 如果需要将类型t中的所有类型取出变成一个联合类型:
type t1 = t[0] // t是定义的一个类型数组,如果取0的话,那么就是会取到第一个元素,也就是number类型,此时t1表示number类型
type t1 = t[1] // string类型
... // 等等
type t1 = t[number] // 那么这里的0、1、这些数字就可以使用number类型进行表示,因为number类型是这些数字类型的父类型,他包括了这些子类型,所以他可以取到整个数组里面所有的值

五、断言

说明: 在某个场景下,你会比TypeScript更加明确知道某个值是什么类型的时候,你就可以指定这个值是什么类型,这种操作就称为类型断言,但是你的代码大量出现断言,那么你的代码就存在问题

1.非空断言(!):

说明: 当某个值它的类型中存在null或者undefined这种类型的时候,TypeScript可能会在检测的时候会出现报错(例如获取页面已经存在的节点的时候),这时候可以在表达式的末尾加上!用来排除null和undefined带来的影响(在你非常确定的时候才可以这样做,不然会给你带来很多问题)

// 例如这里不加这个!可能会在你下面使用这个节点的时候提醒你这个div可能是一个null
const div = document.getElementById('app')! 

2.类型断言(as):

(1)as const

let a = {
  a: 1, // number
};

let b = {
  a: 1, // 1,同时还readonly
} as const;

as const 在使用的时候不仅会缩小类型的范围,而且还会递归数据的成员,将其每一个成员设置成readonly

(2)缩小范围

说明: 下面的常用写法(使用as)和老写法(使用<>)的效果是一致的,都可以用来缩小类型的范围,只不过使用会出现安全问题,尽量避免去使用它

let a: any = "Hello, World!";

// 将a的类型确认为string类型,
let b : number = (a as string).length;
let c : number = (<string>a).length;

六、泛型

1.基本使用:

说明: 一般自己写函数的时候都会知道参数类型以及返回值类型,但是有些情况,你是不清楚的,就像下面这个函数一样

// 定义一个函数签名
type Filter = {
  (array: number[], f: (item: number) => boolean): number[];
  (array: string[], f: (item: string) => boolean): string[];
  (array: object[], f: (item: object) => boolean): object[];
};

// 使用这个签名实现函数filter
const filter: Filter = (array, f) => {
  array.map((item) => {
    console.log(f(item));
  });
  return array;
};

// 如果你跟我一样,在函数filter上可能会出现以下提示

// 不能将类型“(array: number[] | string[] | object[], f: ((item: number) => boolean) | ((item: string) => boolean) | ((item: object) => boolean)) => number[] | string[] | object[]”分配给类型“Filter”。  
// 不能将类型“number[] | string[] | object[]”分配给类型“number[]”。  
// 不能将类型“string[]”分配给类型“number[]”。  
// 不能将类型“string”分配给类型“number”。ts(2322)

写上面这个的时候我还是在学习阶段,不知道为啥会出现这样的错误,所以我换了一种写法,也就是这里提到的泛型

// filter函数使用了一个泛型参数T,也叫做泛型,
// 最开始是不清楚这个T是什么类型的,但是TS可以
// 推导出来,在调用filter函数时,根据参数的类型
// 从而确定T的类型,推导出来后,会将T出现的每一处
// 替换为推导出来的类型
type Filter = <T>(array: T[], f: (item: T) => boolean) => T[];

泛型就像一个占位符,类型检查器会根据上下文来填充具体的类型,习惯上使用T表示,当然你也可以根据自己的喜好来使用,对于泛型的声明,会使用到奇怪的的<>表示,可以理解为Type关键字,由于泛型的出现,他让函数与接受具体类型的函数更加强大,值得注意的是,<>所存在的位置不一样,其限定泛型作用域也不一样

2.绑定时间:

说明: 声明泛型的位置不仅限定泛型的作用域,还决定ts什么时候为泛型绑定具体的类型

type Filter = <T>(array: T[], f: (item: T) => boolean) => T[];

let filter: Filter = (array, f) => {
  return array;
};

<T>在调用签名中声明(位于签名开始的圆括号前面),ts会在调用FIlter类型的函数时为T绑定具体的类型

type Filter<T> = (array: T[], f: (item: T) => boolean) => T[];

let filter: Filter<number> = (array, f) => {
  return array;
};

如果把T的作用域限定在类型别名Filter中,ts会在使用Filter时显式绑定类型

type Filter = <T>(array: T[], f: (item: T) => boolean) => T[];

type Filter = {
  <T>(array: T[], f: (item: T) => boolean): T[];
};

注意:上面这两种写法是等价的,第二种如果写完整的函数签名但是签名只有一个的情况下,可能会提示你最好更改成第一种的写法

3.泛型声明:

说明: 根据2最后的说法,那么在函数签名里面常用的形式有以下几种

// 一个完整的调用签名,T的作用域在单个签名里面,
// 因此,ts将在调用filter类型的函数时为签名中的
// T绑定具体的类型,每次调用都将为T独立绑定类型
// (有点局部的味道)
type Filter = {
  <T>(array: T[], f: (item: T) => boolean): T[];
};

// 是上面的简写形式
type Filter = <T>(array: T[], f: (item: T) => boolean) => T[];

// 一个完整的调用签名,T的作用域覆盖全部签名,
// 由于T是filter类型的一部分,而不属于某个具
// 体的签名,因此ts会在声明filter类型的函数时
// 绑定T(有点全局的味道)
type Filter<T> = {
  (array: T[], f: (item: T) => boolean): T[];
};

// 是上面的简写形式
type Filter<T> = (array: T[], f: (item: T) => boolean) => T[];

// 一个具名函数调用签名,T的作用域在签名里面,
// ts将在调用filter时为T绑定具体类型,每次调
// 用filter也是独立为T绑定类型
function filter<T>(array: T[], f: (item: T) => boolean): T[] {}

4.泛型推导

说明: 多少情况下,ts能够自动推导出泛型,当然,也可以选择自己显式注解泛型,不过显式注解的时候,必须所有的泛型都注解,不然会提示错误的

// 这里实现按照map函数仿写一下
const map = <T, U>(array: T[], f: (item: T) => U): U[] => {
  let result = [];
  for (let i = 0; i < array.length; i++) {
    result[i] = f(array[i]);
  }
  return result;
};

// 得到T为number,U为string
map([1, 2, 3], (_: any) => String(_));

// 显式注解,必须全部注解
map<number, string>([1, 2, 3], (_: any) => String(_));

// 只注解某一个会报错
map<number>([1, 2, 3], (_: any) => String(_));

// 类型输入注视不对会提示错误
map<string, number>([1, 2, 3], (_: any) => String(_));

5.泛型别名

说明: 类型别名只有一个地方可以声明泛型,就是在名称之后,等号之前定义

type MyEvent<T> = {
  target: T;
  type: string;
};

当然,泛型别名也可以嵌套使用来构建其它的类型

type TimedEvent<T> = {
  event: MyEvent<T>;
  from: Date;
  to: Date;
};

在使用这样的泛型时,必须显式绑定类型参数,因为ts无法自行推导

let myEvent: MyEvent<HTMLButtonElement | null> = {
  target: document.querySelector("#myButton"),
  type: "click",
};

泛型别名也可以在函数中使用

const triggerEvent = <T>(event: MyEvent<T>): void => {};

// 这个函数在执行的时候需要一个对象作为参数,根据函数签名,
// ts会发现参数类型是MyEvent<T>,根据定义的泛型类型MyEvent<T>
// 发现其结构为{ target: T; type: string; },其次根据传入的参
// 数target的值为document.querySelector("#myButton"),得到T的
// 类型是Element | null,然后检查全部代码将T的类型替换掉,
// 最后确认所有类型都满足可赋值性,确保代码的类型是安全的
triggerEvent({
  target: document.querySelector("#myButton"),
  type: "click",
});

七、枚举(非必要不使用)

1.说明:

说明: 枚举(类型)允许将一组相关的常量组织在一起,并为每个常量赋予一个易于理解和记忆的名称,在编译的时候可以提供类型的检查,并且是可复用的,因此,枚举提升了代码的可读性和可维护性,枚举的类型有数字枚举字符串枚举异构枚举,并且在书写的时候枚举名称和枚举的键的首字母大写

2.反向映射:

说明: 在 TypeScript 中,枚举默认情况下是双向映射的,即成员名称到成员值的正向映射,以及成员值到成员名称的反向映射。这使得可以通过成员名称或成员值来访问枚举成员。

// 比如下面的ts在编译成JS会发生变化:
enum Status {
  Uploading,
  Success,
  Failed
}

// 编译后的js:
var Status;
(function (Status) {
    // 以第一句为例,从等式左边向右看的话,Status["Uploading"] = 0
    // 这也是在使用成员名来进行枚举取值,此时,这个等式的返回值是0,
    // 也就会得到另一个等式,也就是Status[0] = "Uploading",这变相
    // 的得到了使用成员值来获取成员名,这就是双向的获取,也就是反向
    // 映射
    Status[Status["Uploading"] = 0] = "Uploading";
    Status[Status["Success"] = 1] = "Success";
    Status[Status["Failed"] = 2] = "Failed";
})(Status || (Status = {}));

注意:反向映射只在数值枚举中有效,而字符串枚举不会生成反向映射。这是因为字符串枚举的成员没有与之相关联的数值。

3.计算枚举成员:

说明: 在 TypeScript 中,枚举成员的值可以是计算得出的结果。通过给枚举成员赋予一个表达式,可以动态计算并确定成员的值。

`手动赋值:`

enum Size {
  Small = 1,
  Medium = 2,
  Large = 3 + 4, // 7
  XLarge = Math.pow(2, 3), // 8
}

手动给枚举成员赋予一个表达式作为其值。这个表达式可以是任意有效的 TypeScript 表达式。

`自动增长:`

enum Days {
  Monday = 1,
  Tuesday, // 值为 2
  Wednesday, // 值为 3
  Thursday = 10,
  Friday, // 值为 11
}

对于没有显式赋值的枚举成员,TypeScript 会自动递增其值。自动递增的规则是基于上一个有值的枚举成员的值进行递增,并且计算枚举成员的表达式在编译时会被计算并转换为相应的值,所以在编译出来的js代码中成员值是一个具体的值而不是一个表达式

4.常量成员:

说明: 常量成员是指具有初始值并且在编译时被视为常量的枚举成员。

// 举例:常量枚举
enum Colors { 
    Red = '#FF0000',
    Green = '#00FF00',
    Blue = '#0000FF',
}

1.常量成员必须有初始值:那也就是说他不可以像自动递增的枚举成员那样省略初始值
2.在编译时会将其定为常量:在得到的js中会被硬编为常量,那么他在运行时是不可以变的
3.可以使用常量表达式:常量成员的初始值可以是常量表达式

5.const枚举:

说明: const 枚举是TypeScript中的一种枚举声明方式,它与常规枚举有所不同,常规枚举在编译为 JavaScript 时会生成一个"真实"的对象。这意味着在运行时,常规枚举的值是可以被访问的。然而,在某些情况下,我们希望在编译为 JavaScript 时直接使用枚举成员的值,而不生成真实的对象。这时可以使用 const 枚举。

const enum Colors {
  Red = "#FF0000",
  Green = "#00FF00",
  Blue = "#0000FF"
}

let color: Colors = Colors.Red;
console.log(color); // 输出:#FF0000

// 定义了一个名为color的变量,并将其类型声明为Colors枚举。
// 然后将该变量的值设置为Colors.Red。在这个例子中,Colors
// 是一个枚举类型,其中包含了三个成员:Red、Green和Blue。
// 通过将枚举成员赋值给变量color,我们将变量的值设为Colors.Red,
// 即枚举的红色成员。通过这样的赋值操作,变量color现在持有了一个
// 叫做Colors.Red的枚举成员,在后续的代码中可以使用color这个变量,
// 根据需要进行处理。此时color变量的类型限制为Colors枚举,即它只
// 能接受Colors枚举中定义的成员作为值,这样可以在编译阶段进行类型
// 检查,确保只有正确的枚举成员可以被赋值给该变量。

1.在编译时被内联化:使用枚举成员的地方直接替换成对应的值。整个枚举声明会被移除掉,不会保留枚举的数据结构。
2.只能包含常量成员:只能有初始值并且在编译时被视为常量的枚举成员。
3.无法使用计算成员:const 枚举无法使用计算成员,因为计算成员的值只能在运行时确定,而 const 枚举的值在编译时就要确定。

6.数字枚举:

说明: 数字枚举的特点在于变量会自动的增加,但是,需要注意的是,数字枚举在设定值的时候,可以使用计算值和常量。但是要注意,如果某个字段使用了计算值或常量,那么该字段后面紧接着的字段必须要设置初始值。

// 这是一个简单的数字枚举,如果我没有去初始化值的话,那么第一个变量也
// 就是North它的默认值是0,让后依次增加,分别为2、3、4
enum Direction {
  North,
  South,
  East,
  West
}

`对于枚举的取值:`
// 如果通过成员值来获取:
Direction[0] // North
Direction[1] // South

// 如果通过成员名来获取:
Direction.North // 0
Direction.South // 1
`使用常量的枚举:`
const test = 1;
enum Status {
  Uploading = 3,
  Success = test,
  Failed, // Error:枚举成员必须具有初始化表达式,所以这里必须有初始值
}
`使用计算值的枚举:`
const getIndex = () => 3
enum Status {
  Uploading = 2,
  Success = getIndex(),
  Failed = 5 // 这里需要存在初始化的值
}

7.字符串枚举:

说明: 字符串枚举是一种特殊类型的枚举,在 TypeScript 中用于定义一组相关的常量,其中每个成员都是字符串类型,与常规的数字枚举不同,字符串枚举的成员没有与之关联的数值,而是使用字符串作为标识符。每个字符串枚举成员都必须是一个字符串字面量,而不能是变量或函数的返回值。

// 定义一个字符串枚举
enum MyStringEnum { 
    Member1 = "Value1",
    Member2 = "Value2",
    Member3 = "Value3",
}

// 字符串枚举的取值:直接通过成员明访问成员的值
MyStringEnum.Menber1 // Value1
MyStringEnum.Menber2 // Value2
MyStringEnum.Menber3 // Value3

注意:字符串枚举在取值的时候是不存在反向映射的

8.异构枚举:

说明: 异构枚举就是指一个枚举里面既有字符串类型的成员,也有数字类型的成员,不过在使用的时候需要小心的管理,避免混淆。

`成员类型混合:`

enum Status {
  Active = 1,
  Inactive = "inactive",
  Pending = 2,
}

异构枚举可以包含数字和字符串类型的成员

`自动递增:`
        
enum Status {
  Active = 1,
  Inactive = "inactive",
  Pending, // 自动递增,值为 2
  Paused, // 自动递增,值为 3
}

对于异构枚举中的数字成员,如果没有显式赋值,则会自动递增,递增的规则与普通的数字枚举相同,但注意不会影响字符串成员的赋值

最后,由于字符串之间不存在反向映射,所以在异构枚举中字符串类型不可以通过成员的值去获取成员名

八、类型约束

说明: 在使用泛型的时候,通常都是这是一个泛型T,那是一个泛型U,这并不能满足我们使用的需求,比如我需要泛型T必须拥有泛型U相似的结构等等这样的需求时,就可以使用下面这些关键字来做一下约束了

1."键入"运算符:

举例: 假设有这样的一个嵌套的对象类型,如果我想获取这里面friendList的类型FriendList怎么办

type APIResponse = {
    user: {
        userId: string
        friendList: {
            count: number
            friends: {
                firstName: string
                lastName: string
            }[]
        }
    }
}

Maybe: 可能你会这样写(将其拆开),比如:

type FriendList = {
    count: number
    friends: {
        firstName: string
        lastName: string
    }[]
}

type APIResponse = {
    user: {
        userId: string
        friendList: FriendList
    }
}

"键入"类型: 其实可以写的简单点,这种就是"键入"类型

type FriendList = APIResponse['user']['friendList']

任何结构(对象、类构造方法和类的实例)和数组都可以按这种写法,number是"键入"数组类型的方式,若是元组,数字字面量表表示想"键入"的索引,但是在查找属性类型的值的时候,只能够使用[],而不能使用.

2.keyof:

说明: 获取对象所有键的类型,合并为一个字符串类型

(1)基本使用

type APIResponse = {
    user: {
        userId: string
        friendList: {
            count: number
            friends: {
                firstName: string
                lastName: string
            }[]
        }
    }
}

// user
type ResponseKeys = keyof APIResponse

// userId | friendList
type UserKeys = keyof APIResponse['user']

// count | friends
type FriendListKeys = keyof APIResponse['user']['friendList']

(2)"键入"与keyof结合

说明: 可以实现对类型安全的读取函数,读取对象中指定键的值

实现:

function get<O extends object, K extends keyof O>(o: O, k: K): O[K] {
  return o[k];
}

type ActivityLog = {
  lastEvent: Date;
  events: {
    id: string;
    timestamp: Date;
    type: "read" | "write";
  }[];
};

let activityLog: ActivityLog = {
  lastEvent: new Date(),
  events: [{ id: "zhangsan", timestamp: new Date(), type: "read" }],
};

// Date
let lastEvent = get(activityLog, 'lastEvent')
  • 函数的参数有两个,分别是o和k,对应的类型是O和K
  • 类型O是扩展的对象object,那他就是一个对象类型了,那么keyof O就能够得到这个对象里面所有键组成的并集类型了
  • 类型K是扩展的keyof O,也就是并集类型的子类型了
  • 返回值类型是 O[K],表示对象 o 的属性 k 的类型。函数内部使用 o[k] 来访问对象 o 的属性 k,并返回该属性的值

升级: 这里用重载来让这个函数可以传递三个参数

type Get = {
  <O extends object, K1 extends keyof O>(o: O, k1: K1): O[K1];
  <O extends object, K1 extends keyof O, K2 extends keyof O[K1]>(
    o: O,
    k1: K1,
    k2: K2
  ): O[K1][K2];
  <
    O extends object,
    K1 extends keyof O,
    K2 extends keyof O[K1],
    K3 extends keyof O[K1][K2]
  >(
    o: O,
    k1: K1,
    k2: K2,
    k3: K3
  ): O[K1][K2][K3];
};

type ActivityLog = {
  lastEvent: Date;
  events: {
    id: string;
    timestamp: Date;
    type: "read" | "write";
  }[];
};

let get: Get = (object: any, ...keys: string[]) => {
  let result = object;
  keys.forEach((k) => {
    return (result = result[k]);
  });
  return result;
};

let activityLog: ActivityLog = {
  lastEvent: new Date(),
  events: [{ id: "zhangsan", timestamp: new Date(), type: "read" }],
};

// Date
get(activityLog, 'lastEvent')

// {
//     id: string;
//     timestamp: Date;
//     type: "read" | "write";
// }
get(activityLog, 'events', 1)

// Date
get(activityLog, 'events', 0, 'timestamp')

3.typeof:

说明: 能够获取ts推导出来的类型,它操作的是变量,是一个值

let d = {
    name: "zhangsan"
    age:20
}

type D = typeof d // 这里的D就会得到name的类型是string,age的类型是number

// D的类型如下
D {
    name: string
    age: number
}

// 获取d中属性的联合类型:
type DU = typeof d[keyof typeof d] // string | number
// 定义一个函数,这个函数有两个参数,第一个参数是一个对象,第二个参数是该对象的
// 一个属性,函数返回值是该对象对应属性的属性值

// 第一个参数需要被限制成为一个对象的类型,第二个参数被限制成只能是该对象的属性名
function getValue<D extends {[key: string]: any}, K extends keyof D>(data: D, key: K): D[k] {
    return data[key]
}

getValue({a: 1}, "a") // 1

4.in:

(1)映射类型

说明: 通过in遍历联合类型得到的这个类型被称为映射类型,一个对象只能有一个这个类型,这是一种在对象的键和值的类型之间建立联系的一种方式

type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri'
type Day = Weekday | 'Sat' | 'Sun'

// type NextDay = {
//     Mon: Day;
//     Tue: Day;
//     Wed: Day;
//     Thu: Day;
//     Fri: Day;
// }
type NextDay = {
    [K in Weekday]: Day
}

let nextDay: NextDay = {
    Mon: 'Thu',
    Tue: "Mon",
    Wed: "Mon",
    Thu: "Mon",
    Fri: "Mon"
}

(2)作用

举例: 以这个类型为例

type Account = {
    id: number
    isEmployee: boolean
    notes: string[]   
}

所有字段可选:

// type OptionalAccount = {
//     id?: number | undefined;
//     isEmployee?: boolean | undefined;
//     notes?: string[] | undefined;
// }

type OptionalAccount = {
    [K in keyof Account]?: Account[K]
}

所有字段可以为null:

// type NullableAccount = {
//     id: number | null;
//     isEmployee: boolean | null;
//     notes: string[] | null;
// }

type NullableAccount = {
    [K in keyof Account]: Account[K] | null
}

所有字段只读:

// type ReadonlylAccount = {
//     readonly id: number;
//     readonly isEmployee: boolean;
//     readonly notes: string[];
// }

type ReadonlylAccount = {
    readonly [K in keyof Account]: Account[K]
}

这里是可以使用+-运算符的,可以这么理解,readonly+readonly是一样的效果,那也就是说-readonly跟没有加这个readonly这个修饰符是一致的了

5.extends:

说明: 泛型约束的一种,它表示前面类型T必须拥有后面U类型或者比U类型更具体的结构

单个约束

type TreeNode = {
    value: string
}

type LeafNode = TreeNode & {
    isLeaf: true
}

type InnerNode = TreeNode & {
    children: [ TreeNode ] | [ TreeNode, TreeNode ]
}

let a: TreeNode = {
    value: 'a'
}

let b: LeafNode = {
    value: 'a',
    isLeaf: true
}

let c: InnerNode = {
    value: 'a',
    children: [b]
}

// extends: 泛型约束的一种,它表示前面类型T必须拥有后面U类型或者比U类型更具体的结构
function mapNode<T extends TreeNode> (node: T, f: (value:string) => string): T {
    // T extends TreeNode 它规定了泛型T的基础结构,
    // 如果格式不对会存在红色的波浪线来提示
    
    // 如果只使用T,运行时会出错,因为T类型没有了限制条件,
    // 假设读取的node.value的值是一个数字,可能会导致程序出现问题,这是不安全的
    return {
        ...node,
        value: f(node.value)
    }
}

// { value: 'A' }
let a1 = mapNode(a, _ => _.toUpperCase())

// { value: 'A', isLeaf: true }
let b1 = mapNode(b, _ => _.toUpperCase())

// { value: 'A', children: [ { value: 'a', isLeaf: true } ] }
let c1 = mapNode(c, _ => _.toUpperCase())

多个约束

type HasSides = {
    numberOfSides: number
}

type SideHaveLenght = {
    sideLength: number
}

function logPerimeter<T extends HasSides & SideHaveLenght>(s: T): T {
    console.log(s.numberOfSides * s.sideLength);
    return s
}

type Square = HasSides & SideHaveLenght
let square: Square = {
    numberOfSides: 1,
    sideLength: 2
}

logPerimeter(square) // 2

参数不固定

function call(f: (...args: unknown[]) => unknown, ...args: unknown[]): unknown {
  return f(...args);
}

// 函数f接受一系列T类型的参数,返回某种类型R,但f的参数具体有多少个是不知道的
// 函数call的参数为f,f也接受一些T类型的参数,同样其数量也是未知的,其函数的返回值类型也是R
function call<T extends unknown[], R>(f: (...args: T) => R, ...args: T): R {
  return f(...args);
}

function fill(length: number, value: string): string[] {
  return Array.from({ length }, () => value);
}

let a = call(fill, 10, 'a')
let b = call(fill, 10)
let c = call(fill, 10, 'a', 'v')

默认值

// 这个类型用于描述dom事件
type MyEvent<T> = {
  target: T;
  type: string;
};

// 新建事件的时候,需要显式绑定一个泛型表示触发事件的HTML元素的类型
let buttonEvent: MyEvent<HTMLButtonElement> = {
  target: myButton,
  type: string,
};

// 当然事先如果不知道是什么类型也可以给定泛型一个默认值
type MyEvent<T = HTMLElement> = {
  target: T;
  type: string;
};

// 这个泛型T也是可以使用extends关键字进行约束的
type MyEvent<T extends HTMLElement = HTMLElement> = {
  target: T;
  type: string;
};

// 泛型默认值跟函数参数默认值一样,有默认值得需要放在没有默认值的后面
type MyEvent3<Type extends string, Target extends HTMLElement = HTMLElement> = {
  target: Target;
  type: Type;
};

九、模块

1.import、export

非必要情况,ts的代码应该使用es6中的import和export导入导出模块,而不是使用CommonJS、全局和命名空间中的模块

说明: import 和 export 是js中用于模块化开发的关键字,只不过前者是用来导入模块,后者用于导出模块

(1)常规操作

导入导出:

// a.ts
export function foo(){}
export function bar(){}

// b.ts
import { foo, bar } from '导入的文件路径'
foo()

默认导出:

// a.ts
export default function foo(){}

全量导出: 使用*来一次性导入所有的内容,后面的as表示为这部分重新起个名字

// a.ts
export function foo(){}
export function bar(){}
export * from '导出的文件路径'

// b.ts
import * as a from '导入的文件路径'
a.foo()
a.bar()

这里只是列举了一些,像变量,类型这些都是可以导入导出的

(2)动态导入(理论)

说明: 当一个文件过大的时候,导入需要的时间可能要很久,这时可以使用拆分的方法将其拆分成多个利用浏览器的迸发进行下载,另一个就是懒加载,也就是动态加载,这需要使用到import关键字,当做语句使用的时候,会返回一个Promise

import可以做一个语句来使用,下面是两种安全写法:

  • 直接把字符串字面量传给import,但不要事先赋值给变量
  • 把表达式传递给import,但是需要手动注解模块签名
// 这样就可以使用 async await了
import('文件路径')
// 模块 ./module.ts
export function greet(name: string): void {
  console.log(`Hello, ${name}!`);
}

// 在另一个文件中,执行动态导入并手动注解模块签名
const moduleName = './module';
const modulePromise: Promise<typeof import(moduleName)> = import(moduleName);

// 使用导入的模块
modulePromise.then((module) => {
  module.greet('John');
});

2.声明合并

看图说话:

枚举函数类型别名接口命名空间模块
-
类型--
枚举---
函数----
类型别名-----
接口------
命名空间-------
模块-------

十、内置的工具

1.映射类型

(1)Record

说明: ts内置的类型用于描述有映射关系的对象,语法格式为Record<KeyType, ValueType> ,其中 KeyType 是键的类型,ValueType 是值的类型

type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri'
type Day = Weekday | 'Sat' | 'Sun'

// 这里会报错,因为它缺少了Tue, Wed, Thu, Fri
let nextDay: Record<Weekday, Day> = {
    Mon: 'Thu'
}
  • KeyType 是一个联合类型,那么记录类型 Record<KeyType, ValueType> 将需要满足联合类型中的所有情况。

注意: 这是Record在ts中实现的方式。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
  • 由于KeyType是继承keyof any的,那么他的类型只能为string | number | symbol的子类型

(2)Partial

说明: 它用于创建一个包含目标类型所有属性的可选类型,语法格式为Partial<Type>Type表示需要转换的类型

type Account = {
  id: number;
  isEmployee: boolean;
};

// type PartialAccount = {
//     id?: number | undefined;
//     isEmployee?: boolean | undefined;
// }

type PartialAccount = Partial<Account>

(3)Required

说明: 它用于创建一个包含目标类型所有属性的必需类型,语法格式为Required<Type>Type表示需要转换的类型

type Account = {
  id: number;
  isEmployee: boolean;
};

// type RequiredAccount = {
//     id: number;
//     isEmployee: boolean;
// }

type RequiredAccount = Required<Account>

(4)Readonly

说明: 它用于创建一个包含目标类型所有属性的只读,语法格式为Readonly<Type>Type表示需要转换的类型

type Account = {
  id: number;
  isEmployee: boolean;
};

// type ReadonlyAccount = {
//     readonly id: number;
//     readonly isEmployee: boolean;
// }

type ReadonlyAccount = Readonly<Account>

(5)Pick

说明: 它从目标类型中选择指定的属性并创建一个新的类型,语法格式为Pick<Type,ChooseType>Type表示目标类型,ChooseType表示选择的属性名的字符串字面量或联合类型

type Account = {
  id: number;
  isEmployee: boolean;
};

// type PickAccount = {
//     id: number;
// }

type PickAccount = Pick<Account, 'id'>

2.条件类型

(1)Exclude

说明: 用于从联合类型中排除指定的类型,其语法格式为Exclude<T,U>T表示一个联合类型,U表示联合类型需要排除的类型

type a = string | number
type b = string

// number
type c = Exclude<a,b>

(2)Extract

说明: 语法格式为Extract<T,U>,用于计算T中可以赋值给U的类型

type a = string | number
type b = string

// string
type c = Extract<a,b>

(3)NonNullable

说明: 用于排除T中的nullundefined类型,语法格式为NonNullable<T>

type a = string | null | undefined

// string
type b = NonNullable<a>

(4)ReturnType

说明: 用于计算函数的返回值类型,这个不适用于泛型函数和重载函数,其语法格式为ReturnType<T>T表示需要计算的函数类型

type a = () => number

// number
type b = ReturnType<a>

(5)InstanceType

说明: 用于获取构造函数类型的实例类型,其语法格式为InstanceType<T>T 是一个泛型类型参数,需要传入一个构造函数类型

class Book {
  title: string;
  author: string;

  constructor(title: string, author: string) {
    this.title = title;
    this.author = author;
  }
}

// 通过使用 InstanceType,我们能够轻松获得构造函数的实例类型,
// 并在代码中使用它进行类型检查和操作
type BookInstance = InstanceType<typeof Book>;

const book: BookInstance = new Book("The Great Gatsby", "F. Scott Fitzgerald");
console.log(book.title); //  "The Great Gatsby"
console.log(book.author); //  "F. Scott Fitzgerald"

(6)Omit

说明: 用于删除一个类型的指定属性并返回一个新类型,语法格式为Omit<K,T>,其中K表示需要删除属性的类型,删除的TK中的属性

type Person {
  name: string;
  age: number;
  address: string;
}

type PersonWithoutAge = Omit<Person, 'age'>;

const person: PersonWithoutAge = {
  name: 'John',
  address: '123 Main St'
};

十一、class

1.属性的限制

说明:TypeScript中,我们可以使用 privateprotected 和 public 来定义类的属性的可访问性。

// 在这个例子里面中,name是一个私有属性,无法在类外部访问;
// age是一个受保护属性,只能在类及其子类中访问;color
// 是一个公共属性,可以在任何地方访问。我们还定义了 
// getName()方法来获取私有属性 name的值,以及 getAge()
// 方法来获取受保护属性 age的值。在 Dog类中,我们可以通过
// 继承来访问父类的受保护属性和公共属性。

class Animal {
  private name: string;
  protected age: number;
  public color: string;

  constructor(name: string, age: number, color: string) {
    this.name = name;
    this.age = age;
    this.color = color;
  }

  public getName(): string {
    return this.name;
  }

  protected getAge(): number {
    return this.age;
  }
}

class Dog extends Animal {
  public getInfo(): string {
    return `Name: ${this.name}, Age: ${this.getAge()}, Color: ${this.color}`;
  }
}

const animal = new Animal("Leo", 3, "brown");
console.log(animal.name); // 错误,私有属性无法访问
console.log(animal.age); // 错误,受保护属性无法访问
console.log(animal.color); // 可以访问

const dog = new Dog("Max", 5, "white");
console.log(dog.getInfo()); // 可以访问父类的受保护属性和公共属性
console.log(dog.color); // 可以访问父类的公共属性

通常情况下,class内部属性是不应该让外界随便访问的,但是方法是可以的,所以属性默认设置成private,方法默认设置成public

2.抽象类

说明: 抽象类是一种基础类,不能被直接实例化,需要使用 abstract 关键字来定义,其次抽象类里面的方法只有抽象方法抽象方法必须出现在子类里面并使用abstract关键字定义,并且抽象方法只存在函数签名但不会具体实现和非抽象方法可以被子类继承使用

class Position {
    constructor(private files: Files, private rank: Rank) {}
    distanceFrom(position: Position) {
        return {
            rank: Math.abs(position.rank - this.rank),
            files: Math.abs(position.files.charCodeAt(0) - this.files.charCodeAt(0))
        }
    }
}

abstract class Piece {
    // Piece的子类必须实现满足这函数签名的方法,否则ts就会检测数来并提醒你
    abstract canMoveTo(position: Position): boolean
}

class King extends Piece {
    canMoveTo(position: Position): boolean {
        let distance = this.position.distanceFrom(position)
        return distance.rank < 2 && distance.files < 2
    }
}

3.implements关键字

说明: 声明类的时候,可以使用implements关键字来指明类满足某个接口,这是为类添加约束的一种方式

interface Animal {
  eat(food: string): void;
}

// Cat需要实现接口Animal里面的每个方法,
// 当然也可以在此基础上实现其它属性和方法
class Cat implements Animal {
  eat(food: string) {
    console.log(food);
  }
}

接口可以声明实例属性,但是属性不能带有privateprotectedpublic修饰符,也不能使用static关键字,当然,可以像对象类型那样使用readonly将实例属性标记为只读,最后就是一个类可以实现多个接口

interface Animal {
  eat(food: string): void;
}

interface Feline {
  meow(): void;
}

class Cat implements Animal, Feline {
  eat(food: string) {
    console.log(food);
  }
  meow() {
    console.log("meow");
  }
}

4.类是结构化类型

说明: ts中的类是否与其它类型是否兼容,要看结构,就好比常规对象定义了同样的属性或者方法,也是与类兼容的

class Zebra {
  trot() {}
}

class Poodle {
  trot() {}
}

function ambleAround(animal: Zebra) {
  animal.trot();
}

let zebra = new Zebra();
let poodle = new Poodle();

ambleAround(zebra);
ambleAround(poodle);

但是如果类中有private或者protected字段,并且结构不是类或其子类的实例,那么这个结构就不能可赋值给类

class A {
  private x = 1;
}

class B extends A {}

function f(a: A) {}

f(new A());
f(new B());

// 报错
f({ x: 1 });