开始
什么是Typescript?
融合了后端面向对象思想的超级版的javascript语言,为javascript补充了类型系统
环境搭建
mkdir demo
cd demo
npm init -y
npm i pnpm -g
# 局部安装,也可以全局安装,但建议局部安装
pnpm add typescript -D
npx tsc --init
优势
编译时静态类型检测:函数或方法传参或变量赋值不匹配时,会出现编译错误提示,规避了开发期间的大量低级错误,省时,省力。
自动提示更清晰明确
引入了泛型和一系列的TS特有的类型
强大的d.ts声明文件:声明文件像一个书的目录一样,清晰直观展示了依赖库文件的接口,type类型,类,函数,变量等声明
轻松编译成JS文件:即使TS文件有错误,绝大多数情况也能编译出JS文件
灵活性高:尽管TS是一门强类型检查语言,但也提供了any类型和 as any断言
类型注解和类型推导
类型注解就是明确指定类型,定义变量的时候就指定类型
let a:number = 1
类型推导就是根据给的值推断类型
let a = 1
TS编译和编译优化
ts代码需要编译成js代码才能执行
# 使用tsc编译
tsc 文件
# 编译所有
tsc
# 编译时有错误不输出
tsc --noEmitOnError
可以通过tsconfig.json配置编译输出目录"outDir"
通过ts-node来直接编译运行一体化
# 安装ts-node,推荐局部安装
pnpm add ts-node -D
# 使用ts-node编译ts文件
npx ts-node ./src/index.ts
24种常用的TS数据类型
基本类型
number、string、boolean、symbol、null、undefined
null和undefined
null 表示什么都没有,表示一个空对象引用 typeof null === 'object'
声明一个变量,但没有赋值,该变量的值为 undefined typeof undefined === 'undefined'
typescript中需要显示的声明,或者关闭ts配置文件的严格模式检查,不推荐
let str: string | undefined = undefined
// 使用?号声明的变量或参数,也可以接受undefined
function fn (data?: string) {
// 忽略data为undefined
data!.toString()
// data为undefined则不执行
data?.toString()
// 或者先判断再使用
if (data) {
data.toString()
}
}
可以接受undefined的值的有三种类型:any、unKnown、undefined
可以接受null的值的有三种类型:any、unKnown、null
根类型
Object、{} 根类型可以是其它所有数据类型的父类,除了null和undefined不能赋值以外,其他都可以。{}和Object是一样的,一个是简写
let data: Object = 3
let data: Object = [3, 3]
let data: Object = new Set<string>()
对象类型
Array、object、function
let data:object = {age: 18}
数组和数组元素,如何同时为只读?as const,使用const时arr不能改变,但其元素可以改变,使用as const会同时约束元素也不可改变
枚举 enum
为什么要用枚举?
它可以解决多次if/switch判断中值的语义化问题
使用常量解决有局限性:方法参数不能定义为具体类型,如下例,只能初级使用number,string基本类型替代,降低了代码的可读性和可维护性,这个时候就需要用到枚举。
const Status = {
MANAGER_ADUIT_FAIL: -1,
NO_ADUIT: 0,
MANAGER_ADUIT_SUCCESS: 1,
FINAL_ADUIT_SUCCESS: 2,
};
class MyAduit {
getAduitStatus(status: number): void {
if (status === Status.MANAGER_ADUIT_FAIL) {
console.log("经理审核未通过");
} else if (status === Status.NO_ADUIT) {
console.log("未审核");
} else if (status === Status.MANAGER_ADUIT_SUCCESS) {
console.log("经理审核通过");
} else if (status === Status.FINAL_ADUIT_SUCCESS) {
console.log("终审审核通过");
}
}
}
枚举定义、取值、分类
定义,使用关键字
enum
// 字符串枚举
enum StatusEnum {
MANAGER_ADUIT_FAIL = "经理审核未通过",
NO_ADUIT = "未审核",
MANAGER_ADUIT_SUCCESS = "经理审核通过",
FINAL_ADUIT_SUCCESS = "终审审核通过",
}
// 数字枚举, 默认从0开始, 也可以手动指定, 从1开始, 后面的值会自动递增
enum Week {
Monday = 1,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
取值:枚举既是一个数据类型,也是一个变量,取值的时候可以直接使用
// 取值, 可以通过枚举的值取到对应的key, 也可以通过key取到对应的值(只能是数字枚举,双向映射)
console.log(StatusEnum.MANAGER_ADUIT_FAIL); // 经理审核未通过
console.log(StatusEnum.NO_ADUIT); // 未审核
console.log(StatusEnum.MANAGER_ADUIT_SUCCESS); // 经理审核通过
console.log(StatusEnum.FINAL_ADUIT_SUCCESS); // 终审审核通过
console.log(Week.Monday); // 1
console.log(Week.Tuesday); // 2
console.log(Week.Wednesday); // 3
console.log(Week.Thursday); // 4
console.log(Week.Friday); // 5
console.log(Week.Saturday); // 6
console.log(Week.Sunday); // 7
console.log(Week[1]); // Monday
console.log(Week[2]); // Tuesday
console.log(Week[3]); // Wednesday
console.log(Week[4]); // Thursday
console.log(Week[5]); // Friday
console.log(Week[6]); // Saturday
console.log(Week[7]); // Sunday
枚举底层,上面的枚举编译成js代码后,可以看到数字枚举的双向映射实现
// 字符串枚举
var StatusEnum;
(function (StatusEnum) {
StatusEnum["MANAGER_ADUIT_FAIL"] = "\u7ECF\u7406\u5BA1\u6838\u672A\u901A\u8FC7";
StatusEnum["NO_ADUIT"] = "\u672A\u5BA1\u6838";
StatusEnum["MANAGER_ADUIT_SUCCESS"] = "\u7ECF\u7406\u5BA1\u6838\u901A\u8FC7";
StatusEnum["FINAL_ADUIT_SUCCESS"] = "\u7EC8\u5BA1\u5BA1\u6838\u901A\u8FC7";
})(StatusEnum || (StatusEnum = {}));
// 数字枚举, 默认从0开始, 也可以手动指定, 从1开始, 后面的值会自动递增
var Week;
(function (Week) {
// 此处 Week["Monday"] = 1 返回 1,然后 Week[1] = "Monday"; 生成的 Week 对象就是 { 1: "Monday", "Monday": 1 },从而实现双向取值
Week[Week["Monday"] = 1] = "Monday";
Week[Week["Tuesday"] = 2] = "Tuesday";
Week[Week["Wednesday"] = 3] = "Wednesday";
Week[Week["Thursday"] = 4] = "Thursday";
Week[Week["Friday"] = 5] = "Friday";
Week[Week["Saturday"] = 6] = "Saturday";
Week[Week["Sunday"] = 7] = "Sunday";
})(Week || (Week = {}));
枚举的好处
有默认值和可以自增值,节省编码时间
语义更清晰,可读性增强
因为枚举是一种值类型的数据类型,方法参数可以明确参数类型为枚举类型
其他特殊类型
any、unknown、never、void、元组(tuple)、可变元组
void: 怎么理解void类型
never:什么都没有的数据类型,使用never避免出现未来扩展新的类没有对应类型的实现,目的就是写出类型绝对安全的代码。
type DataFlow = string | number
function dataFlowAnalysisWithNever(dataFlow: DataFlow) {
if (typeof dataFlow === "string") {
...
} else if (typeof dataFlow === "number") {
...
} else {
// 这个时候data就是never类型,未来扩展一个boolean类型时,这个就是布尔类型
let data = dataFlow
}
}
any和unknown
any和unknown在开发中和第三包源码底层经常看到,弄清楚它们的区别很重要
区别
相同点:any和unknown 可以是任何类的父类,所以任何类型的变量都可以赋值给any或unknown类型的变量
不同点1:any 也可以是任何类的子类,但unknown不可以,所以any类型的变量都可以赋值给其他类型的变量
不同点2:不能拿unknown类型的变量来获取任何属性和方法,但any类型的变量可以获取任意名称的属性和任意名称的方法
any应用场景
自定义守卫
// Vue3 源码片段
// any的应用场景--自定义守卫中使用any
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true) // any类型的 r 参数在函数内部获取属性
}
需要进行 as any 类型断言的场景
unknown应用场景
一般用作函数参数:用来接受任意类型的变量实参,但在函数内部只用于再次传递或输出结果,不获取属性的场景
function ref(value?: unknown) {
return createRef(value) // 函数内部只用于再次传递值,不获取属性,也就是不操作
}
Object 和 any的区别
any
类型是完全不受类型系统约束的,可以赋予任何类型的值,也可以从中访问任何属性和方法而不会有编译时检查。使用any
可以绕过TypeScript的类型检查,但这样做会失去TypeScript提供的大部分类型安全性和自动化工具支持的好处。Object
类型是一个顶级类型(但不是最顶级的,最顶级的是unknown
),比any
类型受到更多的限制。你可以将任何类型的值赋给Object
类型的变量,但是当你尝试访问该变量上的任何方法或属性时,TypeScript会报错,除非它能够确定该操作是安全的。这意味着,与any
不同,使用Object
类型时,TypeScript会进行一定程度的类型检查。简而言之,any类型几乎放弃了所有的类型检查,而Object类型则保留了一些基本的检查以确保操作的安全性。在实践中,应当尽量避免使用any类型,以利用TypeScript提供的类型安全性。如果确实需要一个可以接受任何类型值的变量,应该考虑使用unknown类型,因为它比any类型更安全。
null和undefined可以赋值给any和unknown,但不能赋值给Object
扩展阅读
元组(tuple)
满足以下3点的数组就是元组
在定义时每个元素的类型都确定
元素值的数据类型必须是当前元素定义的类型
元素值的个数据必须和定义时个数相同
const user: [string, number] = ["Tom", 28];
可变元组和它的应用场景
// 可变元组:数组和元组的结合
// 元组是固定长度的数组,而数组是可变长度的
// 通过元组和数组的结合,可以实现可变长度的元组
const customers: [string, number, string, ...any[]] = [
"Tom",
25,
"123",
"456",
"789",
];
// 可变元组解构
const [k, age, ...rest] = customers;
console.log(rest);
可变元组标签
给元组的元素加个标签,一眼就能看出这个元素是啥,非常好用
const customers: [tag_name: string, tag_age: number, string, ...any[]] = [
"Tom",
25,
"123",
"456",
"789",
];
// 要解构使用
const [tag_name] = customers;
console.log(tag_name); // Tom
合成类型
联合类型、交叉类型
// 联合类型
let str: string | number | boolean = false
// 交叉类型
type Obj1 = {username: string}
type Obj2 = {age: number}
type Obj3 = Obj1 & Obj2
let obj1:Obj1 = {username: 'cgf'}
let obj2:Obj2 = {age: 18}
let obj3:Obj3 = { username: 'cgf', age: 18 }
# 这是错误的,字符串跟数字无法交叉,会报错never
let str string & number = ''
字面量数据类型
简单的说就是把值当成类型,如 1 , 'a'
let n: 1 = 1
type num = 1 | 2 | 3
let a:num = 1
type IncreaseFlag = 0 | 1
接口和应用场景
接口:另一种定义对象类型的类型,也就是定义出来的是一种对象类型,但有自己的语法规则
应用场景
一些第三方包或者框架底层源码中有大量的接口类型
提供方法的对象类型的参数时使用
为多个同类别的类提供统一的方法和属性声明
如何定义接口
接口中只有声明,没有具体实现,接口可以继承,面向对象思想,可以用类来实现接口。思考:接口重名会合并,那么在代码中可以声明三方库接口同名的接口来扩展吗?
interface Bird {
fly(): void;
layEggs(): void;
}
interface Bird extends Animal {
fly(): void;
layEggs(): void;
}
interface List {
add(): void;
remove(): void;
}
class ArrayList implements List {
add(): void {
console.log("add");
}
remove(): void {
console.log("remove");
}
}
class LinkedList implements List {
add(): void {
console.log("add");
}
remove(): void {
console.log("remove");
}
}
継承接口
新的接口只是在原来接口继承之上增加了一些属性和方法,这时就用接口继承
可索引签名
interface Product {
name: string;
price: number;
account: number;
// 可索引签名,当写索引签名时,值的类型必须兼容上面的类型,也就是说直接写number的话,会报错。只有兼容string和number的类型才可以
[key: string]: any;
}
type Name = Product["name"];
let p: Product = {
name: "apple",
price: 10,
account: 100,
// 加了可索引签名,当key是字符串时可以添加任意属性,当key是number时,不能是字符串, 当key是symbol时,只能是symbol,虽然可以,但一般使用字符串
color: "red",
// 不要以为只能是字符串,可以是数字
1: "1",
// symbol也行
[Symbol("aaa")]: "aaa",
true: true,
};
索引访问类型
const symid = Symbol("id");
interface Product {
[symid]: string | number;
name: string;
price: number;
account: number;
[key: string]: any;
}
// 需要注意的是,Product是类型,name也是类型,不是字符串,所以不能直接使用Product.name
type A = Product["name"];
type B = Product["price" | "name"];
type C = Product[typeof symid]; // 索引访问类型,只能是类型,不能是变量,用symid就会报错
// 获取所有的key
type K = keyof Product;
// 获取所有的value
type V = Product[keyof Product];
// 如何查看更清晰的类型
// 条件类型(Conditional Types),这是TypeScript中的一个高级类型特性。条件类型允许你根据类型之间的关系定义类型,形式为T extends U ? X : Y,意味着如果类型T可以赋值给类型U,则类型为X,否则为Y。
type AllKeys<T> = T extends any ? T : never;
type AllKeysType = AllKeys<keyof Product>;
函数和函数类型,rest参数
函数声明
function getUserInfo(name: string, age: number): string {
return `${name} ${age}`;
}
const getUserInfo2 = function (name: string, age: number): string {
return `${name} ${age}`;
};
const getUserInfo3 = (name: string, age: number): string => {
return `${name} ${age}`;
};
函数类型声明
const fn: (name: string, age: number) => string = function (name, age) {
return `${name} ${age}`;
};
type FnType = (name: string, age: number) => string;
const fn2: FnType = function (name, age) {
return `${name} ${age}`;
};
rest参数
function getUserInfo4(name: string, age: number, ...rest: any[]): string {
return `${name} ${age} ${rest.join(" ")}`;
}
函数类型解构
function getUserInfo5({ name, age, phone }: fnObj): string {
return `${name} ${age} ${phone}`;
}
函数复杂实战:简单的Promise片段
// 先不使用泛型
type Resolve = (value?: unknown) => void;
type Reject = (reason?: unknown) => void;
type Executor = (resolve: Resolve, reject: Reject) => void;
class MPromise {
public resolve: Resolve = (value) => {
console.log(value);
};
public reject: Reject = (reason) => {
console.log(reason);
};
constructor(executor: Executor) {
executor(this.resolve, this.reject);
}
}
const promise = new MPromise((resolve, reject) => {
resolve("success");
reject("fail");
});
// npx ts-node ./src/Promise.ts 输出
// success
// fail
interface 和 type的区别
type 和 接口类似,都用来定义类型,type也可以为类型别名,两者区别如下:
区别1:定义类型范围不同
interface 只能定义对象类型或接口当名字的函数类型
type 可以定义任何类型,包括基础类型、联合类型、交叉类型,元组
type num = number;
type baseType = string | number | boolean;
interface Car {
brand: string;
price: number;
run(): void;
}
interface Plane {
brand: string;
price: number;
fly(): void;
}
type Vehicle = Car | Plane;
// 元组
type TupleVehicle = [Car, Plane];
区别2
接口可以extends一个或者多个接口,或类实现一个或者多个接口,也可以继承type
但type没有继承功能
interface List {
add(): void;
remove(): void;
}
interface ArrayListInf extends List {
add(): void;
remove(): void;
update(): void;
}
// 仅用作示例
interface LinkedList extends List, ArrayList {
add(): void;
remove(): void;
update(): void;
search(): void;
}
// 仅用作示例
class ArrayList implements ArrayListInf, List, LinkedList {
add(): void {
console.log("add");
}
remove(): void {
console.log("remove");
}
update(): void {
console.log("update");
}
}
区别3
用type交叉类型 & 可让类型中的成员合并成一个新的type类型
接口不能交叉合并
type Group = {
name: string;
num: number;
};
type Info = {
info: string;
age: number;
};
type User = Group & Info;
const user: User = {
name: "name",
num: 1,
info: "info",
age: 18,
};
区别4:接口可合并声明
定义两个相同名称的接口会合并声明
定义两个同名的type会出现编译错误
interface Error {
code: number;
message: string;
}
interface Error {
data: string;
}
const error: Error = {
code: 1,
message: "error",
data: "data",
name: "自定义", // 全局Error接口中合并来的
};
类、静态属性、何时用静态属性?
定义:类就是拥有相同属性和方法的一系列对象的集合
理解:类是一个模具,是从这类包含的所有具体对象中抽象出来的一个概念,类定义了它所包含的全体对象的静态特征和动态特征。如人类、动物等。
ES6和ts中的类还是有一些不一样,如private封装
class People {
private name: string;
private age: number;
private address: string;
// 静态成员(静态属性或静态方法都是静态成员): 只能通过类名来访问,不能通过实例来访问,全局共享,文件加载时就会初始化到内存中
public static count: number = 0;
constructor(name: string, age: number, addr: string) {
this.name = name;
this.age = age;
this.address = addr;
People.count++;
}
sayHello() {
return `Hello, ${this.name} ${this.age} ${this.address}`;
}
}
const p = new People("Tom", 18, "Beijing");
const p1 = new People("Tom1", 18, "Beijing");
const p2 = new People("Tom2", 18, "Beijing");
const p3 = new People("Tom3", 18, "Beijing");
console.log(p.sayHello());
console.log(People.count);
企业项目何时使用静态成员?某些方法或属性全局需要使用,不需要每次实例化一个实例去使用的这些方法,都可以使用表态成员,实现有两种方式
静态成员:方法或属性加static属性
单例模式:只会实例化一次,第二次实例化会返回已缓存实例
class DateUtil {
static format() {}
static utc() {}
static parse() {}
}
单件(例)模式的两种实现
静态属性直接导出class的方式,提示会寻找原型链,会有很多方法,通过单例模式创建的实例,只有当前实例的方法
export class DateUtil {
static format() {}
static utc() {}
static parse() {}
}
单例模式实现一
class DateUtil {
static dateUtil = new DateUtil();
// 构造方法私有化,防止外部实例化
private constructor() {}
static format() {}
static utc() {}
static parse() {}
}
export default DateUtil.dateUtil;
单例模式实现二
直到想获取实例时才创建,而不是一开始就创建
class DateUtil {
static dateUtil: DateUtil;
static getInstance() {
if (!DateUtil.dateUtil) {
DateUtil.dateUtil = new DateUtil();
}
return DateUtil.dateUtil;
}
// 构造方法私有化,防止外部实例化
private constructor() {
console.log("DateUtil constructor");
}
format() {}
utc() {}
parse() {}
}
const util1 = DateUtil.getInstance();
const util2 = DateUtil.getInstance();
console.log(util1 === util2); // true
const util3 = DateUtil.getInstance();
const util4 = DateUtil.getInstance();
TS类getter,setter使用和意义
直接操作类的属性时,我们无法对其进行验证和限制,使用getter,setter就可以进行控制
class People {
private name: string;
private _age: number = 18;
private address: string;
// 静态成员(静态属性或静态方法都是静态成员): 只能通过类名来访问,不能通过实例来访问,全局共享,文件加载时就会初始化到内存中
public static count: number = 0;
constructor(name: string, addr: string) {
this.name = name;
this.address = addr;
People.count++;
}
// 年龄的设置应该进行控制,总不能设置1000岁吧
set age(value: number) {
this._age = value > 0 && value < 150 ? value : 0;
}
get age() {
// 获取年龄的时候,可以对年龄进行一些处理,有时候要保密
return this._age > 30 ? 18 : this._age;
}
sayHello() {
return `Hello, ${this.name} ${this.age} ${this.address}`;
}
}