Visual LabINTERACTIVE LEARNING
JavaScript 内核精校教程

TypeScript 类型系统

从基础类型进入联合、泛型、映射与条件类型,建立“用类型表达约束”的思维。

TypeScript泛型类型体操
进阶预计 48 分钟查看源文 ↗

提示

js类型 缺失,ts作为js的超集,用于类型检查,提升开发效率和代码安全性,能及早发现错误

tsconfig 配置文件

.json .js .ts 三种文件格式都可以识别

🦄 提示

作用:

  1. 编译时选项,tsc 或者webpack中的ts-loader (实际在编译时使用的babel,这个文件不会影响)
  2. 代码编辑器的提示
{
"compilerOptions": {},
"files":[],//用于指定编译的文件数组,文件较少时使用
"include":['src/**/*.ts'],//包含的文件
"exclude":[],//优先于include,排除的文件
}

环境搭建

pnpm add typescript -g

pnpm add  ts-node  @type/node

类型推导

🎁 提示

声明变量的时候,直接赋值,会根据赋值的类型推导出标识符的类型注解,称为类型推导

  • let 声明的变量类型推导会是通用类型
  • const 声明的变量类型推导,推导出来的是字面量类型

type 数据类型

📌 提示

ts中有一些预先定义的类型 如 Iterable 可迭代类型

提示

注意! ts 中number小写,是指number类型

  • number 小写,ts 中的类型 js 中Number 是一个类,是用来创建实例的
  • Number 大写js 中的数据类 ,ts 中也可以用

🏆 提示

**类型推断:**ts 会自动推断变量的类型,如果定义时没有赋值,不管之后有没有赋值,都会被推断成 any 类型,会报错

布尔类型
let isEnd: boolean = false;
// isEnd只能是布尔类型,不能是其他类型
数字类型
let num: number = 2;
字符串类型
let str: string = "1";
数组类型
let arr: string[] = ["1"];
// 数组中只能放字符串

let arr1: Array<number> = [1, 2, 3];
// 写法2,很少用
void 类型
function getData(a: string | number, b: string): void {
  console.log("void");
}
// void类型的变量只能赋值为undefined和null 没有返回值的函数
//一个函数没有返回值时,返回值是void
//通常使用在定义函数的数据类型的地方使用
type func:()=> void

//当基于上下文类型推导中的函数,推导出来的函数返回值是viod时,如果返回一个值,并不会报错
对象类型
let obj:{name:string} = {
  name: 123,
};

// 直接写,利用类型推导,不要直接这样写
元组类型
let tuple: [string, number] = ["1", 2];
// 元组类型注意注解的格式,同时必须对应赋值,注意写法,类型写在中括号中
// 函数中使用元组最多,如react中使用useState
any 类型
let mar: any = 1;
// 任意类型,当无法确定类型的时候,或者后面可能会发生改变的时候,使用该类型
//不要滥用any类型,回到js
//做任何操作都是合法的
undefined 类型
// 只有一个值就是undefined
unknown 类型
// 不确定的类型
//和any 的类型的相似,区别是的,unknown类型上做任何操作都是非法的,需要先进行类型缩小,再进行对应操作
never 类型
// 永远不能赋值的类型
//用来先设置,或者函数中有死循环,或者函数中有报错,都会报错
//一般自己不会主动定义,封装工具库时会使用
handleMessage(msg:string|number){
switch(typeof msg){
  case 'String':
     console.log(msg.length)
  case 'Number':
     console.log(msg)
  default:
     const check:never = msg
  }
}

//一旦需要扩展handleMessage方法时,添加了类型,而忘记添加实现,则会报错,增加代码的严谨性
枚举类型
//重要
// type Direction = "left" | "Right" | "Top" | "Bottom"
//LEFT=0  RIGHT=1  TOP=2  BOTTOM=3 这里是有默认值的,实际会对应到一个数值,并且可以赋值
enum Direction {
  LEFT, //LEFT=1,后面可以递增
  RIGHT,
  TOP,
  BOTTOM,
}

function turnDirection(direction: Direction) {
  switch (direction) {
    case Direction.LEFT:
      console.log("改变角色的方向向左");
      break;
    case Direction.RIGHT:
      console.log("改变角色的方向向右");
      break;
    case Direction.TOP:
      console.log("改变角色的方向向上");
      break;
    case Direction.BOTTOM:
      console.log("改变角色的方向向下");
      break;
    default:
      const foo: never = direction;
      //   never的典型应用 当Direction 新增选项而turnDirection没有增加对应代码的时候,会报错
      break;
  }
}

turnDirection(Direction.LEFT);
turnDirection(Direction.RIGHT);
turnDirection(Direction.TOP);
turnDirection(Direction.BOTTOM);
null 类型
// 只有一个值就是null
函数参数的类型
function getData(num: number, str: string): string {
  return num + str;
}
// 函数参数的类型 会做提示
// 作为参数的匿名函数可以不做注解
// 函数的参数对象类型
function getData(obj: { num: number; str?: string }): string {
  console.log(obj.str);
  return obj.num + "";
}

//可选参数的类型,是一个联合类型  原本的类型和 undefined

//默认参数的类型也是联合类型,因为调用的时候也可以传入undefined

//剩余参数的类型注解需要写成数组  
联合类型 和交叉类型
// 用竖线隔开,并集,联合类型,满足其中一种类型即可
number | string | boolean;

//交叉类型 多个类型都需要满足
interface IPerson {
name: string
}

interface IStudent{
 age:number
}
//既需要满足IPerson 也需要满足IStudent
type Ikun = IPerson & IStudent
可选类型
// 参数即为可选
function(message?:string){

}
字符串字面量类型
let myName: "Tom" = "Tom";

type EventNames = "click" | "scroll" | "mousemove";
let event = "click";
// 意义在于结合联合类型才有意义

// 字面量推理
type Request = {
  url: string;
  method: Method;
};

type Method = "GET" | "POST";

const options: Request = {
  // 方法一
  url: "http://baidu.com",
  method: "POST",
} as const; //方法二

// as const 会将options的属性变成只读属性,否则method是字面量类型,option里的method是string类型,
//会报错

function getData(url: string, method: Method) {}

getData(options.url, options.method);
函数类型
//ts对传入的函数参数个数不校验!!!,能少不能多

//函数本身也是一个标识符,自身也有类型
//方案一、函数类型表达式(arg:string)=> void  没有返回值
type addFn = (num:sting,str:number) => void
const  add:addFn = function(num:sting,str:number){
    return num+str
}
//方案二、函数调用签名 (从对象的角度看函数),当需要描述函数其他属性时,使用

interface IFunc {
name:string
(num:number):void //调用签名
new ():Person  //构造签名
}

const  printNumber:IFunc = function(num:number){
}

Class

类的继承

提示

子类可以继承父类中的属性和方法,减少相同代码的重复编写 js/ts中是单继承,只能继承单一父类

类的多态:

父类引用指向子类对象

//亦即父类作为子类的实例的类型,这样可以提前确定好子类实例中拥有哪些方法,
//让中间层可以调用,中间层不再关注具体的实现,只需要调用对应的方法即可
const square: Shape = new Square(10);  //见抽象类中的具体实现

类的封装

提示

紧凑的组织代码,

类成员修饰符

  • 成员修饰符

    • public 公开访问 default
    • private 私有属性 子类和实例不能访问
    • protected 保护属性 子类可以访问( 使用),子类实例不能访问
    • readonly 只能在声明时或构造函数里被初始化,其他地方不能修改
    • static 静态属性,类可以访问,实例不能访问

提示

当构造函数修饰为 protected 时,该类只允许被继承: 当构造函数修饰为 private 时,该类不允许被继承或者实例化

抽象类

❤️ 提示

抽象类不能被实例化,只能被继承 抽象类中的抽象方法必须被子类实现 抽象类中的抽象方法可以没有具体实现,也可以有具体实现

// 抽象类
abstract class Shape {
  abstract getAera(): number;
}

class Circle extends Shape {
  private r: number;
  constructor(radius: number) {
    super();
    this.r = radius;
  }
  getAera() {
    return Math.PI * this.r ** 2;
  }
}

class Square extends Shape {
  private a: number;
  constructor(a: number) {
    super();
    this.a = a;
  }
  getAera() {
    return this.a ** 2;
  }
}

function makeAera(a: Shape) {
  return a.getAera();
}

const circle = new Circle(10);
const square: Shape = new Square(10);
console.log(makeAera(circle));
console.log(makeAera(square));

类的访问器 getter /setter

❤️ 提示

访问器是一种特殊的方法,用来读取或设置某个属性的值 访问器不会被编译到 js 中,只会在编译阶段进行类型检查 访问器不必须有 get 和 set 方法,可以只有 get 方法,也可以只有 set 方法,一般用于拦截私有属性的操作 只实现get 方法,那么该属性为 readonly 访问器的参数不能有修饰符,也不能有默认值

参数属性(TS语法糖)

提示

在constructor方法中,参数前面加上修饰符,即可简化属性的编写

class Person {

  constructor(public name: string, public age: number) {

  }
}

const p1 = new Person('Max', 30);
console.log(p1.name);

类的类型特性

提示

  • 类可以创建实例
  • 类可以作为实例的类型
  • 类可以作为有构造签名的函数- 工厂函数中使用

interface 接口

提示

主要用于声明对象类型,接口中的属性和方法都是抽象的,不需要具体实现

interface Itable {
  head: string;
  body: string;
  foot?: string; //可选属性
  content?: {
    name: string;
    age: number;
  };
}

// 键值对的定义方式 内置的Record 
interface IndexItem {
  [index: string]: string;
}

// 函数类型的接口
interface IAdd {
  (a: number, b: number): number;
}

接口的继承

提示

接口可以继承多个接口

interface ISwim {
  swimming: () = void;
}

interface IFly {
  flying: () = void;
}

interface IAction extends ISwim, IFly {}

const action: IAction = {
  swimming() {},
  flying() {},
};

接口的合并

// 一种组合类型的方式: 联合类型
type WhyType = number | string;
type Direction = "left" | "right" | "center";

// 另一种组件类型的方式: 交叉类型
type WType = number & string;

interface ISwim {
  swimming: () = void;
}

interface IFly {
  flying: () = void;
}

type MyType1 = ISwim | IFly;
type MyType2 = ISwim & IFly;

const obj1: MyType1 = {
  flying() {},
};

const obj2: MyType2 = {
  swimming() {},
  flying() {},
};

接口的实现

interface ISwim {
  swimming: () = void;
}

interface IEat {
  eating: () = void;
}

// 类实现接口
class Animal {}

// 继承: js只能实现单继承
// 实现: 实现接口, 类可以实现多个接口 -类实现 可以比接口定义的属性多
class Fish extends Animal implements ISwim, IEat {
  swimming() {
    console.log("Fish Swmming");
  }

  eating() {
    console.log("Fish Eating");
  }
}

class Person<T implements ISwim<T {
  // 可传泛型
  swimming() {
    console.log("Person Swimming");
  }
}

// 编写一些公共的API: 面向接口编程
function swimAction(swimable: ISwim) {
  swimable.swimming();
}

// 1.所有实现了接口的类对应的对象, 都是可以传入
swimAction(new Fish());
swimAction(new Person());

swimAction({ swimming: function () {} });

interface 和 type 的区别

继承接口 定义多个相同 实现接口 定义函数类型 声明基本类型
type
interface ✅ 内容会合并

❤️ 提示

建议基本类型使用type,对象类型使用interface

泛型

🥖 提示

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,使用一个变量代替,而在使用的时候再指定类型的一种特性 泛型的思想就是把类型抽象成一个变量,在定义函数或者类的时候,传入类型的形参,使用的时候传入根据参数推导具体的类型

图解TS泛型

🌟 提示

泛型的类型限制是通过接口的继承实现的

// 首先定义一个具有长度的类型 接口
interface ILength {
  length: number;
}

// 函数传入的类型形参是继承自ILength的类型,则必需实现了length属性,否则会报错
// 泛型约束
function getLength<T extends ILength(arg: T): number {
  return arg.length;
  // 而且传入的参数类型必须是ILength的子类 ,所以肯定具有length属性
}

// 所以可见,使用泛型具有通用的约束参数类型的作用

// 传入的参数类型也可以是多个或者有默认值的

function getLength2<T extends ILength, K = string>(arg1: T, arg2: K): number {
  return arg1.length + arg2.length;
}

// 调用函数可以不直接写具体的传入类型,函数能够自动推导出类型
getLength2("abc", "def");

泛型约束

  1. 简单约束

interface ILength {
  length: number;
}

// 函数传入的类型形参是继承自ILength的类型,则必需实现了length属性,否则会报错
function getLength<T extends ILength(arg: T): number {
  return arg.length;
  // 而且传入的参数类型必须是ILength的子类 ,所以肯定具有length属性
}
  1. keyof 用来约束类型必须是对象中的一个key
interface IPerson {
  name:string
  age: number
  height:string
}

type PersonKeys = keyof IPerson // 'name' | 'age' | 'height'
//约束K必须是IPerson当中的一个key
function getLength<T extends IPerson,K extends keyof IPerson >(arg: T,arg2:K): number {
  return arg.age;
}
  1. 映射类型 - 使用对象索引签名实现对一个接口的复制,并且对其修改
interface IPerson {
  readonly name: string;
  age: number;
  height: string;
}
//可以使用+ - 来增加或者去除原来类型中的修饰符
// 使用的索引签名 和 类型提取实现类型复制

//需用type
type MapIPerson<T> = {
 - readonly [prop in keyof T]-?: T[prop]   
}

type NewPerson = MapIPerson<IPerson>
//即可复制IPerson的属性,需用type

ts知识扩展

类型断言

  • as 定义为某一个类型,如果需要强制转换,需要先转换成unknown 再转换成所需类型
  • ! 非空断言 值不为空
//1. as关键词
// 用来告诉ts变量的实际类型
//类型断言只能把类型断言成更加(从不太具体的类型断言成)具体,或者(从比较的具体的类型断言成)更加不太具体的类型
class Person{}

class Student extends Person{
    studying() {}
}

function sayHello(p:Person) {
    (p as Student).studying()
    // 把p断言为子类,就可以调用子类的方法
}

let stu=new Student();
sayHello(stu)

//2. 非空断言 ! 
function sayHello(name?: string) {
    //非空断言,保证非空,调用方法
  console.log( name!.length);
}

// 3.扩展
// 1. 可选链 ?. 用来判断是否有值
// 2. !! 转换成布尔值
// 3. ?? 用来判断是否有值,有值就返回,没有值就返回后面的值,比三目运算符更加方便

类型别名

type myType = number | string | boolean;

// 使用type来定义类型别名
type myObjType = {
  name: string;
  age: number;
  height?: number;
};
// 对象别名也可以是对象

类型缩小

提示

缩小类型的范围,从而可以使用特定类型的方法

  • typeof / instanceof / in
  • === !== 平等缩小

//亦即在(联合类型)变量使用的时候,多一层判断,确认最终类型

type IDtype = string |number

function getData(id:IDtype){
    if(typeof id === 'string'){
        //此时id就是string类型
        console.log(id.length)
     }
}

// in的判断方式

type Dog = {
    bark(){}
}
type Cat = {
    Miao(){ }
}
function sound(p:Cat|Dog){
    // 通过in来判断
    if('bark' in p){
        p.bark()
    }else{
        p.Miao()
    }
}

const pet:Dog= {
    bark(){
        console.log('汪汪汪')
    }
}
sound(pet)

函数的重载

// 定义相同名称,参数不同的几个函数签名,只有一个实现函数,ts会根据参数的类型 来判断调用哪个函数
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
  return a + b;
}

console.log(add(10, 20));
console.log(add("10", "20"));
//实际

//能通过联合类型实现的优先使用联合类型,不能实现的才使用函数重载,比较麻烦

鸭子类型

提示

一个对象看起来像鸭子,叫声像鸭子,行为像鸭子,那么可以认为它是一个鸭子

ts中的类型检测使用的鸭子类型

❤️ 提示

鸭子类型是一种动态类型的概念,它是指在运行时,一个对象的有效语义是由它当前方法和属性的集合决定的,而不是由它所继承的特定类或接口的集合决定的。这意味着,只要一个对象具有与所需方法和属性相同的方法和属性,它就可以被视为是所需的类型。这种类型检查方式通常用于动态语言中,如JavaScript和Python

对象和接口中的属性都可以使用的修饰符

  • ?.可选
  • Readonly 只读

对象接口索引签名

相当于一个动态属性

type Person = {
  [index: number]: string  //允许通过索引获取值
  lenght: number
}

interface Person  {
  [index: number]: string; //允许通过索引获取值
  lenght: number;
  [key:string]:string|number  //[]中的类型只能是string 或者number ,不能是其他,联合也不行
};
//1. 可以使用2个索引签名的时候, [key:string]返回的类型必要是[index: number]返回的类型的父类型
//因为number类型最终会转换成string

//2. 其他属性( lenght)也必须符合[key:string]返回的类型

//其实就是一条:其他属性( 包含[index: number])也必须符合[key:string]返回的类型的子类型

字面量赋值

严格对象字面量赋值

在使用字面量对接口或者类型赋值时,直接赋值会严格校验字面量的属性必须和接口保持一致,如下例子

interface IPerson {
  name: string;
  age: number;
  height: number;
}

 const info = {
   name: "why",
   age: 18,
   height: 1.88,
   address: "广州市"
 }

 const p2 : IPerson ={
   name: "why",
   age: 18,
   height: 1.88,
   address: "广州市"
 }
 //这样会报错
 
 const p: IPerson = info
 // freshness擦除 类型检测时
//这样不会报错
// console.log(info)
// console.log(p)

function printInfo(person: IPerson) {
  console.log(person);
}

// 代码会报错
 printInfo({
   name: "why",
   age: 18,
   height: 1.88,
   address: "广州市"
 })

const info = {
  name: "why",
  age: 18,
  height: 1.88,
  address: "广州市",
};
// 使用对象的引用,会在类型检测的时候有擦除,所以不会报错
printInfo(info);

ts的知识扩展

  1. 模块化 支持各种模块化 最推荐的模块化方式是esm

提示

  • js规范声明任何没有export的js文件都会认为是一个脚本,而非一个模块
  • 在一个脚本文件中,变量和类型会被声明在共享的全局作用域,将多个输入文件合并成一个输出文件
  • 如果希望它被作为一个模块处理,需要添加
export {}

这会把文件当成单独的模块处理,有独立的作用域,不会污染作用域

  • 类型的导入导出
//方法一
import type { IPerson } from './type'
//方法二
import { type  IPerson } from './type'

//type.ts
export interface IPerson {
 name:string
}

✍️ 提示

使用type关键词的好处,使用babel、esbuild等编译时,方便识别安全移除,提升效率 不使用也可以

  1. 类型查找 类型声明

🎉 提示

.d.ts文件,主要用于类型声明,不写代码逻辑

  • 内部类型声明 安装typescript后
  • 外部类型声明 自定义代码类型声明
  • 第三方库类型声明
//声明变量
declare const age:number

//声明模块
declare module 'lodash '{
//模块内部需要导出
  export function join(...args:any[]):any
}

//声明静态资源
declare  module '*.jpg'

ts内置工具

详见:TypeScript 内置工具详谈


interface IFunc {
  name: string;
  (this: { name: string }, num: number): void;
}

const printNumber: IFunc = function (this, num) {
  console.log(num);
};

printNumber.name = "print";

//获取函数的类型
type printType = typeof printNumber;
//ThisParameterType获取这个类型的this
type This = ThisParameterType<printType>;

//提取除了this以外的类型
type pureType = OmitThisParameter<printType>;

//绑定上下文的方法
ThisType<printType>;