chengaofeng
发布于 2024-06-30 / 28 阅读
0
0

Typescript核心语法

开始

什么是Typescript?

融合了后端面向对象思想的超级版的javascript语言,为javascript补充了类型系统

环境搭建

mkdir demo
cd demo
npm init -y 
npm i pnpm -g
# 局部安装,也可以全局安装,但建议局部安装
pnpm add typescript -D
npx tsc --init

优势

  1. 编译时静态类型检测:函数或方法传参或变量赋值不匹配时,会出现编译错误提示,规避了开发期间的大量低级错误,省时,省力。

  2. 自动提示更清晰明确

  3. 引入了泛型和一系列的TS特有的类型

  4. 强大的d.ts声明文件:声明文件像一个书的目录一样,清晰直观展示了依赖库文件的接口,type类型,类,函数,变量等声明

  5. 轻松编译成JS文件:即使TS文件有错误,绝大多数情况也能编译出JS文件

  6. 灵活性高:尽管TS是一门强类型检查语言,但也提供了any类型和 as any断言

类型注解和类型推导

  1. 类型注解就是明确指定类型,定义变量的时候就指定类型

let a:number = 1
  1. 类型推导就是根据给的值推断类型

let a = 1

TS编译和编译优化

  1. ts代码需要编译成js代码才能执行

# 使用tsc编译
tsc 文件
# 编译所有
tsc
# 编译时有错误不输出
tsc --noEmitOnError
  1. 可以通过tsconfig.json配置编译输出目录"outDir"

  2. 通过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

  1. null 表示什么都没有,表示一个空对象引用 typeof null === 'object'

  2. 声明一个变量,但没有赋值,该变量的值为 undefined typeof undefined === 'undefined'

  3. typescript中需要显示的声明,或者关闭ts配置文件的严格模式检查,不推荐

let str: string | undefined = undefined

// 使用?号声明的变量或参数,也可以接受undefined
function fn (data?: string) {
  // 忽略data为undefined
  data!.toString()
  // data为undefined则不执行
  data?.toString()
  // 或者先判断再使用
  if (data) {
    data.toString()
  }
}
  1. 可以接受undefined的值的有三种类型:any、unKnown、undefined

  2. 可以接受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

为什么要用枚举?

  1. 它可以解决多次if/switch判断中值的语义化问题

  2. 使用常量解决有局限性:方法参数不能定义为具体类型,如下例,只能初级使用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("终审审核通过");
    }
  }
}

枚举定义、取值、分类

  1. 定义,使用关键字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,
}
  1. 取值:枚举既是一个数据类型,也是一个变量,取值的时候可以直接使用

// 取值, 可以通过枚举的值取到对应的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
  1. 枚举底层,上面的枚举编译成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 = {}));

枚举的好处

  1. 有默认值和可以自增值,节省编码时间

  2. 语义更清晰,可读性增强

  3. 因为枚举是一种值类型的数据类型,方法参数可以明确参数类型为枚举类型

其他特殊类型

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在开发中和第三包源码底层经常看到,弄清楚它们的区别很重要

区别
  1. 相同点:any和unknown 可以是任何类的父类,所以任何类型的变量都可以赋值给any或unknown类型的变量

  2. 不同点1:any 也可以是任何类的子类,但unknown不可以,所以any类型的变量都可以赋值给其他类型的变量

  3. 不同点2:不能拿unknown类型的变量来获取任何属性和方法,但any类型的变量可以获取任意名称的属性和任意名称的方法

any应用场景
  1. 自定义守卫

// Vue3 源码片段
// any的应用场景--自定义守卫中使用any
export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true) // any类型的 r 参数在函数内部获取属性
}
  1. 需要进行 as any 类型断言的场景

unknown应用场景

一般用作函数参数:用来接受任意类型的变量实参,但在函数内部只用于再次传递或输出结果,不获取属性的场景

function ref(value?: unknown) {
  return createRef(value) // 函数内部只用于再次传递值,不获取属性,也就是不操作
}
Object 和 any的区别
  1. any类型是完全不受类型系统约束的,可以赋予任何类型的值,也可以从中访问任何属性和方法而不会有编译时检查。使用any可以绕过TypeScript的类型检查,但这样做会失去TypeScript提供的大部分类型安全性和自动化工具支持的好处。

  2. Object类型是一个顶级类型(但不是最顶级的,最顶级的是unknown),比any类型受到更多的限制。你可以将任何类型的值赋给Object类型的变量,但是当你尝试访问该变量上的任何方法或属性时,TypeScript会报错,除非它能够确定该操作是安全的。这意味着,与any不同,使用Object类型时,TypeScript会进行一定程度的类型检查。

  3. 简而言之,any类型几乎放弃了所有的类型检查,而Object类型则保留了一些基本的检查以确保操作的安全性。在实践中,应当尽量避免使用any类型,以利用TypeScript提供的类型安全性。如果确实需要一个可以接受任何类型值的变量,应该考虑使用unknown类型,因为它比any类型更安全。

  4. null和undefined可以赋值给any和unknown,但不能赋值给Object

扩展阅读
  1. Object 和 unknown 的区别是什么?

  2. 什么是typescript自定义守卫?

元组(tuple)

满足以下3点的数组就是元组

  1. 在定义时每个元素的类型都确定

  2. 元素值的数据类型必须是当前元素定义的类型

  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

接口和应用场景

接口:另一种定义对象类型的类型,也就是定义出来的是一种对象类型,但有自己的语法规则

应用场景

  1. 一些第三方包或者框架底层源码中有大量的接口类型

  2. 提供方法的对象类型的参数时使用

  3. 为多个同类别的类提供统一的方法和属性声明

如何定义接口

接口中只有声明,没有具体实现,接口可以继承,面向对象思想,可以用类来实现接口。思考:接口重名会合并,那么在代码中可以声明三方库接口同名的接口来扩展吗?

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:定义类型范围不同

  1. interface 只能定义对象类型或接口当名字的函数类型

  2. 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

  1. 接口可以extends一个或者多个接口,或类实现一个或者多个接口,也可以继承type

  2. 但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

  1. 用type交叉类型 & 可让类型中的成员合并成一个新的type类型

  2. 接口不能交叉合并

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:接口可合并声明

  1. 定义两个相同名称的接口会合并声明

  2. 定义两个同名的type会出现编译错误

interface Error {
  code: number;
  message: string;
}

interface Error {
  data: string;
}

const error: Error = {
  code: 1,
  message: "error",
  data: "data",
  name: "自定义", // 全局Error接口中合并来的
};

类、静态属性、何时用静态属性?

  1. 定义:类就是拥有相同属性和方法的一系列对象的集合

  2. 理解:类是一个模具,是从这类包含的所有具体对象中抽象出来的一个概念,类定义了它所包含的全体对象的静态特征和动态特征。如人类、动物等。

  3. 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);

企业项目何时使用静态成员?某些方法或属性全局需要使用,不需要每次实例化一个实例去使用的这些方法,都可以使用表态成员,实现有两种方式

  1. 静态成员:方法或属性加static属性

  2. 单例模式:只会实例化一次,第二次实例化会返回已缓存实例

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}`;
  }
}


评论