Visual LabINTERACTIVE LEARNING
JavaScript 内核第 4 / 5 章

泛型与类型收窄

用约束、断言、重载和索引签名表达更精确的类型关系。

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

泛型

🥖 提示

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

图解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'