看完通关TS,不做标题党
0.准备
安装环境
# 全局安装TS
npm i typescript -g
# 检查TS版本
tsc -v
# 安装tsc-node
npm i tsc-node -g
# 生产TS配置文件
tsc --init
1.基础类型
字符串类型
let a: string = 'asdf'
数字类型
JS
中任意数字类型都没问题。let notANumber: number = NaN;//Nan let num: number = 123;//普通数字 let infinityNumber: number = Infinity;//无穷大 let decimal: number = 6;//十进制 let hex: number = 0xf00d;//十六进制 let binary: number = 0b1010;//二进制 let octal: number = 0o744;//八进制s
布尔类型
let flag: boolean = true
注意一点的是使用
new Boolean()
会报错,因为new Boolean()
结果是一个对象。// 不能将类型“Boolean”分配给类型“boolean”。 // “boolean”是基元,但“Boolean”是包装器对象。如可能首选使用“boolean”。 let flag: boolean = new Boolean(1)
空值类型
用来表示一个函数没有返回值。
function fn(): void { console.log('log') }
null
和undefined
类型undefined
表示声明了没有赋值,null
表示声明以后赋值了空值。let u: undefined = undefined;//定义undefined let n: null = null;//定义null
可以把
undefined
赋值给void
:let u: void = undefined
非严格模式下,也可以把
null
赋值给void
,但是严格模式下不行:// 严格模式下会出现报错提示: // 不能将类型“null”分配给类型“void”。 let n: void = null;
非严格模式下
null
和undefined
可以赋值给任何类型,但是在严格模式下null
只能赋值给null
或者any
类型;undefined
只能赋值undefined
,void
,或者any
类型。let n1: any = null let n2: null = null let n3: undefined = null // 报错 let u1: any = undefined; let u2: null = undefined; // 报错 let u3: undefined = undefined;
2.any和unknow
所有的类型都可以赋值给
any
类型,any
类型也可以,是完全跳过了TS的检查。let anyValue: any = 10; let str1: string = anyValue;
所有类型也都可以赋值给
unkonw
类型,但是unknown
类型的值只能赋值给unknown
或any
类型。let unknownValue: unknown = "hello"; let str2: string = unknownValue; // 错误:不能将类型 'unknown' 分配给类型 'string'
any
可以直接操作,不进行类型检查。console.log(anyValue.length); // 允许(即使可能运行时出错) anyValue.callSomeMethod(); // 允许(即使可能运行时出错)
unknown
不能直接操作。console.log(unknownValue.length); // 错误:对象的类型为 'unknown' unknownValue.callSomeMethod(); // 错误:对象的类型为 'unknown'
3.接口和对象类型
在TS中,我们用来约束对象的类型主要用的就是interface
(接口)。
//这样写是会报错的 因为我们在person定义了a,b但是对象里面缺少b属性
//使用接口约束的时候不能多一个属性也不能少一个属性
//必须与接口保持一致
interface Person {
b: string,
a: string
}
// 报错提示少了b属性
const person: Person = {
a: "213"
}
重名的interface
会进行合并:
interface Person {
a: string,
}
interface Person {
b: number,
}
// 最终Person接口定义的类型是:
// interface Person {
// a: string
// b: number,
// }
也可以可以使用extends
继承:
interface A {
a: string,
}
interface B extends A {
b: number
}
// 最终B的定义的接口是
// interface B {
// a: string,
// b: number
// }
可以使用?
来定义这个属性为可选属性,意思就是有没有都行:
interface A {
a: string,
b?: number
}
// 并没有报错提示说少了b属性,因为b是可选的
let a: A = {
a: '123'
}
有一种情况就是我们不知道会对这个对象添加什么属性,这个时候可以使用任意属性:
// 我们定义了[propName: string]: any;
// 允许添加新的任意属性
interface Test {
a?: string,
b: string,
[propName: string]: any;
}
// 我们添加了c属性和d属性都没有出现报错
const obj: Test = {
a: "213",
b: "123",
c: 'asdfasdf',
d: true
}
// 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
// 我们定义了[propName: string]: boolean;
interface Test {
a?: string,
b: string,
[propName: string]: boolean;
}
// 这个时候c属性会报错,因为string并不是boolean的子集
const obj: Test = {
a: "213",
b: "123",
c: 'asdfasdf',
d: true
}
当我们不希望有一些属性会被更改,就在属性前面加上readonly
来表示这个属性可以获取,但是不能改:
interface Test {
readonly a: string,
}
const obj: Test = {
a: "213",
}
// 会有报错提示:无法为“a”赋值,因为它是只读属性。
obj.a = 123
为这个对象添加一个方法可以这样写:
// 用到了前面说过的void,表示这个函数没有返回值
interface Person {
a?: string,
toEat: () => void
}
const person: Person = {
a: "213",
toEat: () => {
console.log(123)
}
}
4.数组类型
用来限制数组的类型
类型
// 自由发挥,例如arr1就表示数字类型的数组 let arr1: number[] = [123] // 字符串类型数组 let arr2: string[] = ['1', '2', '3', '1'] // any类型的数组 let arr3: any[] = [1, 2, 3,] // 定义好类型以后,不能放入其他的类型 let arr4: number[] = [1, 2, 3, '4'] // 会报错,因为有一个字符串 arr4.push('5')// 也会报错,不能通过方法加入不符合类型的元素
数组泛型
关于泛型,后面还会再提到。
let arr1: Array<number> = [1, 2, 3, 4, 5] let arr2: Array<string> = ['1', '2', '3', '4', '5'] let arr3: Array<boolean> = [true, false, true]
用接口表示数组
interface NumberArray { [index: number]: number; } let arr: NumberArray = [1, 1, 2, 3, 5]; //表示:只要索引的类型是数字时,那么值的类型必须是数字。
arguments
类数组function Arr(...args: any[]): void { // args是一个数组,打印可以看到结果 let arr1: any[] = args console.log(arr1) // 但是arguments不是一个数组,它是一个类数组对象,这样写会报错的 let arr2: any[] = arguments console.log(arr2); // 我们应该使用IArguments,这个是TS内置的可以直接使用 let arr3: IArguments = arguments console.log(arr3); } Arr(111, 222, 333)
5.函数类型
// 和定义对象的接口有点像,不能多传,也不能少传
const fn = (name: string, age:number): string => {
return name + age
}
fn('张三',18)
// 也可以通过?来表示这个参数是可选的
const fn2 = (name: string, age?: number): string => {
return name + age
}
fn2('张三', 18)
// 还可以给定默认值,给定了默认值不传也不会报错
const fn3 = (name: string = 'zs'): string => {
return name
}
fn3()
还可以使用interface
来定义函数:
//定义参数 num 和 num2 :后面定义返回值的类型
interface Add {
(num: number, num2: number): number
}
const fn: Add = (num: number, num2: number): number => {
return num + num2
}
fn(5, 5)
interface User {
name: string;
age: number;
}
function getUserInfo(user: User): User {
return user
}
有的时候不知道后面会传入几个参数,可以使用...items:any[]
:
const fn = (array:number[],...items:any[]):any[] => {
console.log(array,items)
return items
}
let a:number[] = [1,2,3]
fn(a,'4','5','6')
函数重载:先声明多个函数签名(不包含实现),然后提供一个实现签名(通常使用更宽泛的类型)来处理所有情况。
// 这里看不懂没有关系,后面会说typeof的使用方法
// 这段代码大概得意思就是说,a和b可能是string,也有可能是number
// 当a: number, b: number的时候返回值就是number
// 当a: string, b: string的时候返回值就是string
// 当a: string, b: number或者a: number, b: string会直接报错提示,因为没有这样的签名
// 函数重载签名
function add(a: number, b: number): number
function add(a: string, b: string): string
// 实现签名(必须兼容所有重载签名)
function add(a: number | string, b: number | string) {
if (typeof a === "number" && typeof b === "number") {
return a + b
}
if (typeof a === "string" && typeof b === "string") {
return a + b
}
}
// 使用:
const num = add(1, 2); // 返回类型是 number
const str = add("Hello", "World"); // 返回类型是 string
6.联合类型 | 交叉类型
联合类型就像是“或”的关系:
// 这就表示了a的类型是string或者number,反正两种都行
let a: string | number = '123'
a = 123
同理,交叉类型像是“且”的关系,寻找两种类型之间的交叉:
// 要注意,必须要有公共交叉部分,不然就是unknow类型
let a: string & number // a的类型为unknow
interface A {
a: string
}
interface B {
b: number
}
// obj的类型为:
// interface Obj {
// a: string,
// b: number
// }
let obj: A & B = {
a: '123123',
b: 123123
}
7.类型断言
类型断言就是告诉TS,我确定这个是什么类型,你不要想太多:
interface A {
run: string
}
interface B {
build: string
}
const fn = (type: A | B): string => {
// 这样写是有警告的,因为type是A或者B类型,但是B类型上没有run方法
return type.run
}
const fn2 = (type: A | B): string => {
// 我们可以使用类型断言告诉TS,我确定传进来的一定是A类型,你不要管我
return (type as A).run
}
假如说有一个数组,数组里面的元素可以随意改变,我们又不想让他改变可以怎么做呢?
// 使用 as const
let arr = [112, 23, 5456,] as const
// 报错提示 无法为“0”赋值,因为它是只读属性。
arr[0] = 789
8.Class类
TS可以对类进行约束:
// 先写一个VueCls准备试下实现Vue类
interface Options {
el: string | HTMLElement
}
interface VueCls {
options: Options
init(): void
}
// 然后写上以下代码,应该会有提示
// implements:实现,实施
class Vue implements VueCls {
}
鼠标一点就能自动的实现了,但是还是会有报错的提示的,因为类中options
没有赋值,我们需要手动写constructor
来完成赋值:
interface Options {
el: string | HTMLElement
}
interface VueCls {
options: Options
init(): void
}
class Vue implements VueCls {
options: Options
// 或者通过!操作符告诉TS这个值一定会有,你不要担心,这样不赋值也不会报错
// options!: Options
constructor(options: Options) {
this.options = options
}
init(): void {
throw new Error("Method not implemented.")
}
}
类的3个修饰符:
public
默认就是public
// 其实默认就是public class Person { public name: string; public constructor(name: string) { this.name = name; } } const person = new Person("Alice"); console.log(person.name); // 可以访问
private
成员仅限当前类内部访问,子类和外部均不可访问。
class Person { private age: number; constructor(age: number) { this.age = age; } getAge() { return this.age; // 类内部可以访问 } } const person = new Person(30); // console.log(person.age); // 错误:属性"age"为私有属性,只能在类"Person"中访问
protected
成员允许在当前类和子类中访问,外部不可访问。
class Person { protected age: number; constructor(age: number) { this.age = age; } } class Employee extends Person { getAge() { return this.age; // 子类中可以访问 } } const employee = new Employee(25); // console.log(employee.age); // 错误:属性"age"受保护,只能在类"Person"及其子类中访问
readonly
属性只能在声明时或构造函数中初始化,之后不可修改。
class Person { readonly name: string; constructor(name: string) { this.name = name; } changeName() { // this.name = "Bob"; // 错误:无法分配到"name",因为它是只读属性 } }
static
class MathHelper { static PI: number = 3.14159; static calculateCircumference(radius: number): number { return 2 * MathHelper.PI * radius; } } console.log(MathHelper.PI); // 通过类名访问静态属性
abstract
// 要注意的是abstract抽象方法只能出现在abstract抽象类中 // 意思就是方法前面加了abstract修饰符,那么类前面也要加abstract abstract class Animal { abstract makeSound(): void; // 抽象方法,没有实现 move(): void { console.log("Moving..."); } } class Dog extends Animal { makeSound() { console.log("Woof!"); } } // const animal = new Animal(); // 错误:无法创建抽象类的实例 const dog = new Dog(); dog.makeSound(); // "Woof!"
9.元组
个人理解其实也是一个数组,不过在定义的时候限制了数组的元素的个数以及类型。
// 这是一个普通的数组,arr的类型为(string | number)[]
let arr = [1223, '123213']
// arr2就可以说是一个元组,限制了数组的类型和个数[string, number, boolean]
let arr2: [string, number, boolean] = ['123123', 123123, true]
// 元组可以越界,但是越界也会受到类型的限制
arr2.push(false) // 可以
arr.push({ a: 123 }) // 会有报错提示:类型“{ a: number; }”的参数不能赋给类型“string | number”的参数。
// 还可以给每个类型加上tag,看上去会更清晰,比如:
let arr3: [name: string, age: number] = ['zs', 18]
// 可以获取对应tag的类型,比如:
type A = typeof arr3[0] // 获取到类型为string
type B = typeof arr3[1] // 获取到类型为number
// 可以决定那个元素是可选的
let arr4: [name: string, age?: number] = ['zs'] // 不写第二个元素不会报错
10.枚举
// number类型的枚举
enum Week {
Monday = 1,
Tuesday,
Wensday,
ThirsDay,
Friday,
Sarturday,
Sunday
}
// 可以看的出数字枚举TS会自动推断出Friday的值为5。
console.log(Week.Friday);// 5
// 数字枚举不仅可以通过key取值,还能通过值取key
console.log(Week[5]);// Friday
enum Week {
Monday = "MyMonday",
Tuesday = "MyTuesday",
Wensday = "Wensday",
ThirsDay = "ThirsDay",
Friday = "Friday",
Sarturday = "Sarturday",
Sunday = "sunday"
}
// 字符串类型的枚举不能推断出值
console.log(Week.ThirsDay); // ThirsDay
// 字符串类型枚举不能通过值取key
console.log(Week["MyMonday"]); // 会报错
11.类型推论|类型别名
类型推论是说TS有能力推断出这个变量的类型是什么
let a = '123' // 推断出a的类型是string
a = 123 // 这个时候就会报错了,因为不能把number赋给string类型
类型别名就是说给一个类型命名,在类型比较复杂的时候会用到
// 假如说我们有一些数据
let a: string | number = 1
let b: string | number = 2
let c: string | number = 3
// 每次都要重复写 string | number 就会很烦
// 用一个TypeData来把string | number装起来,方便使用
type TypeData = string | number
let e: TypeData = 4
let d: TypeData = 5
let g: TypeData = '6'
// extends在type中表示包含
// 下面的代码表示1是否作为number的子类型
type num = 1 extends number ? 1 : 0
12.never
never
类型在 TS中表示永远不会出现的值的类型。它有几个重要的使用场景和应用:
// 函数一运行就抛出错误,这样的就是never
function throwError(message: string): never {
throw new Error(message);
}
// 无限循环也是never
function infiniteLoop(): never {
while (true) {
// 无限循环
}
}
// 在switch的判断中,用来兜底的
// 这样做的好处:可以尝试一下,如果说添加了一个d,但是忘记改fn中的逻辑,那么就会有报错提示,
type A = 'a' | 'b' | 'c'
function fn(value:A) {
switch (value) {
case "a":
break
case "b":
break
case "c":
break
default:
//是用于场景兜底逻辑
const error:never = value;
return error
}
}
13.symbol类型
symbol
类型的值是通过Symbol
构造函数创建的。
// 就算参数一致,构造出来的symbol也是不一样的
const sym1 = Symbol('value')
const sym2 = Symbol('value')
// 严格模式下会出现报错提示:此比较似乎是无意的,因为类型“typeof sym1”和“typeof sym2”没有重叠。
// 这证明了sym1必不可能和sym2相等
console.log(sym1 === sym2);
symbol
类型的数据经常用作对象的key
:
// 就算参数一致,构造出来的symbol也是不一样的
const sym = Symbol('value')
let obj = {
[sym]: 123
}
console.log(obj[sym]); // 123
对象中symbol
类型的key
是没有办法通过遍历拿到的:
const sym = Symbol('value')
let obj = {
[sym]: 123
}
// 控制台并不会输出任何东西
for (const key in obj) {
console.log(key);
}
那么我们如何才能拿到呢?
// Object.getOwnPropertySymbols()
const symKeys = Object.getOwnPropertySymbols(obj)
console.log(symKeys); // 控制台有输出
// Reflect.ownKeys(),可以拿到所有的key,包括symbol类型的key
const objKeys = Reflect.ownKeys(obj)
console.log(symKeys);
14.泛型
泛型也是一种数据类型,有以下特点:
特点一:定义时不明确使用时必须明确某种具体数据类型的数据类型。
特点二:编译期间进行数据类型检查的数据类型。
// 定义这个接口的时候,并不知道value属性是什么类型,就用一个参数代替
interface Ref<T> {
value: T
}
// 定义的时候不明确,但是使用的时候就要明确,需要传入一个类型
let ref: Ref<string> = {
value: "string"
}
再举个例子为什么要这么用呢?
// 假如有这样两个方法
// 观察发现这两个方法几乎一模一样,参数和返回值的类型不一样
// 那有没有什么好的方法只写一个方法就能解决呢?
const fn1 = (a: string, b: string): string[] => {
return [a, b]
}
const fn2 = (a: number, b: number): number[] => {
return [a, b]
}
// 那么这个时候泛型的优势就凸显出来了
// 这里其实还用到了反向推断泛型,自动推出了T的类型为number
// 不反推的话也可以这样用 let arr = fn3<number>(1, 2)
const fn3 = <T>(a: T, b: T): T[] => {
return [a, b]
}
let arr = fn3(1, 2)
更多的例子:
// 类型别名使用泛型
type A<T> = string | number | T
// 接口使用泛型
interface B<T> {
a: T
}
// 函数使用泛型,除了上面用到的箭头函数,也可以写关键词function定义的函数
function fn<T>(a: T, b: T) {
return [a, b]
}
15.泛型约束
// 这样写肯定会报错的,因为有无法相加的可能出现,比如undefined + undefined
const fn = <T>(a: T, b: T) => {
return a + b // 会出现报错提示
}
// 这个泛型也太泛了,需要我们对他进行约束,保证可以相加
// 在泛型中,extends就表示左边的类型是右边类型的子集
const fn2 = <T extends number>(a: T, b: T) => {
return a + b
}
// 再举个例子
interface Len {
length: number
}
//这样做的目的就是保证T里面有length这个属性
const fn3 = <T extends Len>(data: T) => {
return data.length
}
16.keyof
keyof
操作符接收一个对象类型,并返回由该类型的所有键组成的字符串或数字字面量联合类型:
// 使用typeof来得到这个obj的类型,鼠标悬浮可以查看到
type A = {
name: string;
age: number;
id: number;
}
// B类型实际是 'name' | 'age' | 'id'
type B = keyof A
keyof
常常与泛型结合,特别是创建通用类型安全的函数时候:
// 写一个获取对象属性值的函数
// K的类型收到约束,必须是 keyof T的子集
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 这里推出了T的类型为
// {
// name: string;
// age: number;
// address: string;
//}
// 那么K的类型就为 'name'|'age'|'address'
const person = {
name: "zs",
age: 30,
address: "Wonderland"
};
// 类型安全的属性访问
const name1 = getProperty(person, "name"); // 类型为 string
const age = getProperty(person, "age"); // 类型为 number
// 错误:参数'"job"'不能赋给类型'"name" | "age" | "address"'
// getProperty(person, "job");
17.namespace命名空间
先说一下这个功能用的比较少了,大部分都是用ES 模块了,但是还是可以了解一下的。
比如说我们在做一些跨端项目,需要比较清晰的分类方法:
// 假如我们做一个项目,里面又有H5的方法,又有安卓的方法,又有IOS的方法
namespace H5 {
// 需要注意,必须使用export导出这个方法才能在namespace外面使用
export const h5Fn = () => {
console.log('H5方法');
}
}
namespace Android {
export const androidFn = () => {
console.log('Android方法');
}
}
namespace IOS {
export const iosFn = () => {
console.log('IOS方法');
}
}
namespace Test1 {
// 命名空间也是可以嵌套的,同样也需要导出
export namespace Test2 {
export const testFn = () => {
console.log('Test2方法');
}
}
}
H5.h5Fn()
Android.androidFn()
// 调用嵌套命名空间的方法
Test1.Test2.testFn()
// 还有一些规则,命名空间也是可以想interface一样合并的,可以自行测试
18.模块解析
AMD
,CMD
奇奇怪怪的自行了解吧,我们现在常用的模块规范CommonJS
还有ESM
(ES6模块化)。
// CommonJS
// 导出
//-------------------
// math.js
module.exports = {
add: function(a, b) { return a + b; },
multiply: function(a, b) { return a * b; }
};
// 或者单独导出
exports.subtract = function(a, b) { return a - b; };
//-------------------
// 导入
const math = require('./math');
math.add(2, 3); // 5
// ESM
// 导出
//------------------------
// math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// 默认导出
export default function divide(a, b) { return a / b; }
//------------------------
// 导入
import { add, multiply } from './math.js';
import divide from './math.js';
// 导入所有
import * as math from './math.js';
他们俩之间的不同点:
语法
// CommonJS使用 require()引入 module.exports导出 // ESM使用import引入, export导出
加载时机
// CommonJS是运行的时候同步加载,整个模块对象被加载进内存 // ESM是编译时候加载,确定依赖关系,运行的时候异步加载
导入类型
// CommonJS是值的拷贝,如果导出的值在原模块中改变,导入的值不会变 // ESM是对变量本身的引用
环境
// CommonJS node.js中 // ESM 浏览器环境
19.declare声明文件
当我们在项目中安装了axios
时候引入axios
并不会出现什么报错:
// 不会报错
// 按住Ctrl点击axios能进入axios的声明文件
import axios from "axios";
但是当我们安装了一些比较老的库,没有声明文件,就会出现报错。例如express
:
// express下飘红报错了
import express from 'express'
遇到这种情况,可以根据提示中执行npm i --save-dev @types/express
来解决,或者手写声明文件,下面来尝试手写声明文件。
// 首先创建一个typings用来存放声明文件
// 然后在typings下新建一个express.d.ts
// index.ts
import express from 'express'
const app = express()
const router = express.Router()
app.use('/api', router)
router.get('/list', (req, res) => {
res.json({
code: 200
})
})
app.listen(9001,()=>{
console.log(9001)
})
// express.d.ts
// 基本就是哪里缺什么就补什么
declare module 'express' {
interface Router {
get(path: string, cb: (req: any, res: any) => void): void
}
interface App {
use(path: string, router: any): void
listen(port: number, cb?: () => void): void
}
interface Express {
(): App
Router(): Router
}
const express: Express
export default express
}
20.mixin混入
对象的混入
const obj1 = {
name: 'zs'
}
const obj2 = {
age: 18
}
// 1.使用扩展运算符
const obj3 = {
...obj1, ...obj2
}
// 2.使用 Object.assign
const obj4 = Object.assign({}, obj1, obj2)
//注意了这两种方式实际都是浅拷贝(只有第一层的属性会脱离引用)
// 题外话:如何深拷贝呢?使用 structuredClone
const deepcloneObj = structuredClone(obj1)
类的混入
// 定义 Mixin 的构造函数类型
type Constructor<T = {}> = new (...args: any[]) => T;
// 定义可飞行 Mixin
function CanFly<TBase extends Constructor>(Base: TBase) {
return class extends Base {
fly() {
console.log("Flying!");
}
};
}
// 定义可游泳 Mixin
function CanSwim<TBase extends Constructor>(Base: TBase) {
return class extends Base {
swim() {
console.log("Swimming!");
}
};
}
// 组合 Mixin
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
const FlyingSwimmingAnimal = CanSwim(CanFly(Animal));
// 使用
const superDuck = new FlyingSwimmingAnimal("Duck");
superDuck.fly(); // "Flying!"
superDuck.swim(); // "Swimming!"
21.装饰器
首先要把tsconfig.json
中的这两项配置打开(不然可泵会有报错):
然后安装tsc-node
,后面编译运行就用tsc-node 文件名
。
npm install -g typescript@latest
什么是装饰器:看例子
下面这个例子说明了装饰器的好处就是节省了代码,然后也不用去破坏A
的结构。
// 写一个类装饰器函数,会把自动把类的构造函数传入当做第一个参数
const Watcher: ClassDecorator = (target) => {
target.prototype.getParams = <T>(params: T): T => {
return params
}
}
// 用的时候使用@+函数名就可以了
@Watcher
class A {
constructor() {
}
}
const a = new A()
console.log((a as any).getParams('abc')); // abc
装饰器工厂:
说白了就是使用@Watcher
的时候我想传入一个参数咋办:
// 我们就给他再包一层用来接受参数,里面返回一个普通的装饰器函数
const Watcher = (value: string) => {
const fn: ClassDecorator = (target) => {
target.prototype.getParams = <T>(params: T): T => {
return params
}
target.prototype.value = value
}
return fn
}
@Watcher('123')
class A {
constructor() {
}
}
const a = new A() as any
console.log(a.getParams('abc')); // abc
console.log(a.value); // 123
进一步来看看方法装饰器:
const meet = (name: string) => {
const fn: MethodDecorator = (value, key, descriptor) => {
// 可以打印这三个参数看看
console.log(value, key, descriptor);
// value:{} 就是原型对象
// key:getName 对应的方法名
// descriptor:{
// value: [Function: getName],
// writable: true,
// enumerable: false,
// configurable: true
// }
console.log('name', name);
}
return fn
}
class A {
constructor() {
}
@meet('zs')
getName(): string {
return 'ls'
}
}
// 编译过后控制台输出了zs
属性装饰器:
const met: PropertyDecorator = (...args) => {
console.log(args);
}
class A {
@met
name: string = 'zs'
constructor() {
}
}
// 控制台输输出 [ {}, 'name', undefined ]
参数装饰器:
const met: ParameterDecorator = (...args) => {
console.log(args);
}
class A {
constructor() {
}
setParasm(@met name: string = '213') {
}
}
// 控制台输出 [ {}, 'setParasm', 0 ]
// 前面两个就不说了,0就跟表示位置,第一个0,第二个就是1
22.发布订阅
发布订阅就是一种设计模式,是一种思想没有固定的代码。
很多框架都在使用比如Vue
,electron
,还有一些组件通信插件eventBus
,addEventListener
等等。
// 这个其实就是一个发布订阅,这是一个触发器
document.addEventListener('click', function () {
console.log('点击了');
});
// 如何自己实现一个事件呢
document.addEventListener('abc', function () {
// 控制台输出了 点击了abc
console.log('点击了abc');
});
// 创建了一个事件,相当于一个订阅中心
const e = new Event('abc')
// 使用dispatchEvent派发这个事件
document.dispatchEvent(e)
手写来实现发布订阅:
interface I {
events: Map<string, Function[]> // 事件的数组
once: (event: string, callback: Function) => void // 只触发一次的功能
on: (event: string, callback: Function) => void // 订阅
emit: (event: string, ...args: any[]) => void // 派发
off: (event: string, callback: Function) => void // 删除
}
class Eimitter implements I {
events: Map<string, Function[]>
constructor() {
this.events = new Map()
}
// 只触发一次的功能
once(event: string, callback: Function) {
const fn = (...args: any[]) => {
callback(...args)
// 立马回收
this.off(event, fn)
}
this.on(event, fn)
}
on(event: string, callback: Function) {
if (this.events.has(event)) {
// 说明存过了
const callbackList = this.events.get(event)
// 第二次存就把事件push进去
callbackList && callbackList.push(callback)
} else {
// 说明第一次存
this.events.set(event, [callback])
}
}
emit(event: string, ...args: any[]) {
const callbackList = this.events.get(event)
if (callbackList) {
callbackList.forEach((fn) => {
fn(...args)
})
}
}
off(event: string, callback: Function) {
const callbackList = this.events.get(event)
if (callbackList) {
// 说明有这个事件
const index = callbackList.indexOf(callback)
if (index !== -1) {
// 说明有这个事件
callbackList.splice(index, 1)
}
}
}
}
const bus = new Eimitter()
// 定义callback
const fn1 = (b: boolean, n: number) => {
console.log(1, b, n);
}
const fn2 = (b: boolean, n: number) => {
console.log(2, b, n);
}
bus.on('message', fn1)
bus.on('message', (b: boolean, n: number) => {
console.log(2);
})
// 取消订阅fn1
bus.off("message", fn1)
bus.emit('message', false, 1)
bus.once('message2', fn2)
// 控制台可以看到就算触发多次也只会触发一次
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)
23.类型守卫
用一下这些方法,可以看出来,能够把传入的any
类型的数据做一个过滤,过滤到想要的类型,这个也就是类型收缩。
// typeof
// typeof可以用来判断基本数据类型,但是不能用来判断引用数据类型
const isString = (val: any) => typeof (val) === 'string'
console.log(isString('123')); // true
console.log(isString(123)); // false
// instanceof
// instanceof可以用来判断引用数据类型
const isObject = (val: any) => val instanceof Object
const isAarry = (val: any) => val instanceof Array
console.log(isObject({ a: '123' })); // true
console.log(isAarry([11, 2, 3])); // true
// Array.isAarry
// Array.isArray()可以用来判断数组
const isAarry2 = (val: any) => Array.isArray(val)
console.log(isAarry2([12, 2, 3])); // true
类型谓词:
首先来看一个小题目。
// 实现一个函数,该函数可以传入任何类型
// 但是如果是object 就检查里面的属性,如果里面的属性是number就取两位
// 如果是string就去除左右空格
// 如果是函数就执行
const isString = (data: any) => typeof (data) === 'string'
const isNumber = (data: any) => typeof (data) === 'number'
const isFunction = (data: any) => typeof (data) === 'function'
// 一般写这个就是太长一串了Object.prototype.toString.call(data) === '[object Object]'
// 语法糖简写 ({}).toString.call(data) === '[object Object]'
const isObject = (data: any) => ({}).toString.call(data) === '[object Object]'
const fn = (data: any) => {
if (isObject(data)) {
// 不能使用 for in 遍历,因为for in 遍历的是对象的原型链上的属性
// Object.keys(data) 返回一个数组,数组的元素是data对象的属性名
Object.keys(data).forEach(key => {
let val = data[key]
// data[key] 取出data对象的属性值
if (isNumber(val)) {
data[key] = val.toFixed(2)
} else if (isString(val)) {
data[key] = val.trim()
} else if (isFunction(val)) {
data[key]()
}
})
}
return data
}
const obj = {
a: 1.123,
b: ' 123 ',
c: () => {
console.log('fn')
}
}
console.log(fn(obj));
题目不难,但是在写的过程中发现一个问题:
为什么已经使用了自己定义的方法收窄了类型,但是写的时候没有代码提示?而且检查发现val
类型是any
这个时候我们就要使用到类型谓词实现自定义守卫:
// 类型谓词能让TS更好的推断变量的类型
// :data is string 如果返回的是true,那么这个data一定就是string
const isString = (data: any): data is string => typeof (data) === 'string'
const isNumber = (data: any): data is number => typeof (data) === 'number'
const isFunction = (data: any): data is Function => typeof (data) === 'function'
加上类型谓词就发现有代码提示了,而且TS能推断出类型了:
23.协变和逆变
协变:
interface A {
name: string;
age: number;
}
interface B {
name: string;
age: number;
sex: string;
}
// 可以发现B的属性完全覆盖了A的属性
let a: A = { name: 'zs', age: 1 };
let b: B = { name: 'ls', age: 18, sex: 'male' };
// 发现b可以赋值给a,但是a不能赋值给b
// 再通俗一点就是a要的属性b都有,但是b要的属性a不一定有
a = b
b = a // 报错
逆变:
interface A {
name: string;
age: number;
}
interface B {
name: string;
age: number;
sex: string;
}
let fn1 = (a: A) => {
console.log(a);
}
let fn2 = (b: B) => {
console.log(b);
}
// 如果是一个函数的话,就刚好和协变反过来
// 这个就叫做逆变
fn2 = fn1
fn1 = fn2 // 报错
24.泛型工具
TS一共有内置的许多工具,其实翻译一下就很好理解了。
都以对象类型为例子:
// 定义了一个接口Person
interface Person {
name: string;
age: number;
sex: string;
height: number;
weight: number;
}
Partial
部分的
把一个对象类型所有的属性变成可选。
// Partial<T>的作用是将类型T的所有属性变为可选属性 type PersonPartial = Partial<Person>; // Partial<Person>的结果是: // type PersonPartial = { // name?: string | undefined; // age?: number | undefined; // sex?: string | undefined; // height?: number | undefined; // weight?: number | undefined; // }
Required
必须的
把一个对象类型所有的属性变成必须。
// Required<T>的作用是将类型T的所有属性变为必选属性 type RquiredPerson = Required<PersonPartial>; // Required<Person>的结果是:(可以看出,使用之前PersonPartial都是可选属性的,这下又变回来了) // type RquiredPerson = { // name: string; // age: number; // sex: string; // height: number; // weight: number; // }
Pick
选择
把一个对象类型部分属性选出来新组成一个类型。
// Pick<T, K>的作用是将类型T的属性K提取出来 type PickPerson = Pick<Person, 'name' | 'age'>; // Pick<Person, 'name' | 'age'>的结果是: // type PickPerson = { // name: string; // age: number; // }
Exclude
排除
把一个联合类型中排除掉一些类型。
// Exclude<T, U>的作用是将类型T中属于类型U的属性排除掉 type ExcludePerson = Exclude<keyof Person, 'name' | 'age'>; // type ExcludePerson = "sex" | "height" | "weight" // Exclude只能用于联合类型,所以keyof Person是一个联合类型,'name' | 'age'也是一个联合类型
Omit
省略
把一个对象类型部分属性排除掉。
// Omit<T, K>的作用是将类型T中的属性K排除掉 type OmitPerson = Omit<Person, 'name' | 'age'>; // type OmitPerson = { // sex: string; // height: number; // weight: number; // }
Readonly
只读
把一个对象类型所有的属性变成只读。
// Readonly<T>的作用是将类型T的所有属性变为只读属性 type ReadonlyPerson = Readonly<Person>; // type ReadonlyPerson = { // readonly name: string; // readonly age: number; // readonly sex: string; // readonly height: number; // readonly weight: number; // }
Record
记录
创造一个对象类型。
// Record<K, T>的作用是构造一个类型,这个类型的属性名是K中的属性,属性值是T类型 type RecordPerson = Record<'name' | 'age', string>; // type RecordPerson = { // name: string; // age: string; // }
ReturnType
获取这个函数类型的返回值类型
// ReturnType<T>的作用是获取函数类型T的返回值类型 const fn = () => { return 1 } // 这里的typeof用来获取函数的类型 type ReturenTypePerson = ReturnType<typeof fn>; // ReturnType<() => Person>的结果是: // type ReturenTypePerson = number
25.infer
infer
是 TypeScript 中的一个关键字,用于在 条件类型 中推断类型。它的作用是从一个复杂类型中提取出某个部分的类型。
心语语法:T extends infer U ? U : never
// 推断出数组中的元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;
// 示例
type Numbers = number[];
type NumberType = ElementType<Numbers>; // number
// 提取出出函数返回值(实现ReturnType)
type ReturnType1<T> = T extends (...args: any[]) => infer R ? R : never;
// 示例
function foo() {
return 42;
}
type FooReturnType = ReturnType1<typeof foo>; // number
// 提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// 示例
function bar(a: string, b: number) {
console.log(a, b);
}
type BarParams = Parameters<typeof bar>; // [string, number]