TypeScript快速上手—泛型&&接口&&类

217 阅读24分钟

ts编译

浏览器无法识别ts,ts需要通过tsc编译为js,才可以被浏览器识别

// 安装ts就默认有tsc功能了
npm install typescript -g

// 查看版本
tsc --version

tsc体验

// 通过ts编译产生js文件代码
tsc xxx.ts

ts运行环境

不可能每次手动编译成js,再手动导入到html中,然后再通过html跑在浏览器,所以采用下面两种方案:

  1. 安装ts-node插件(可以帮助我们直接自动编译成js,再让js直接跑在node上)
  2. 使用webapck进行配置(项目中推荐)

1. ts-node

npm install ts-node -g

// 但是ts-node依赖两个包
npm install tslib @types/node -g

安装完毕之后,直接使用ts-node跑代码:

ts-node xxx.ts

2. webpack配置ts-node

需要安装ts-loader插件进行转换:

// 这里安装ts-loader,还需要安装typescript, 因为ts-loader是利用typescript对代码做编译

// 就像安装babel-loader时,需要安装babel-core一样;npm install babel-loader @babel/core; 因为babel-loader要利用babel-core对代码做转化

npm install ts-loader typescript -D

接着需要在webpack中进行ts-loader配置:

// webpack.config.js
rules: [
    {
        test: /\.ts$/,
        loader: "ts-loader"
    }
]

配置完成之后,直接运行会报错,因为配置了ts-loader之后,需要有一个tsconfig.json文件,它是一个ts的配置文件;可以借助以下命令生成:

// 自动生成tsconfig.json文件
tsc --init

有时候可能报错,ts不支持import引入时的ts后缀名;所以可以在在extensions中配置,默认增加.ts

ts变量声明

image.png

var name = "alice"
let age = 18
const len = 1.88

// string是ts中的字符串类型
// String是js中字符串包装类型
const msg:string = "hello"


代码规范:

  • eslint:是让js代码规范

  • tslint:是让ts代码规范

可以全局安装npm install tslint -g,然后使用tslint --init自动生成tslint.json配置文件


类型推导:

let msg = "hello"
msg = 10 // 这里会报错,初始时虽然没有指定string,但是类型推导会将string作为msg的默认指定类型

ts类型

1. string

2. number

3. boolean

4. Symbol

5. Array

// 方式1
const names: Array<string> = []  // 不推荐(react的JSX中是有冲突的)

// 方式2
const names: string[] = [] // 推荐

6. Enum

enum NameTypeEnum {
  CITY_LEVEL = "城市等级",
  COMPETING_PRODUCTS_LAYOUT = "布局",
  MEMORABILIA_NODE = "节点"
}



7. any

//eg
let str: string ="hello"
str = 123  // 报错,因为指定了只能是string;如果想后期给str赋值任何类型的值,可以修改为any


// eg
let str: any ="hello"
str = 123

两种场景:

  1. 当进行一些类型断言
  2. 不想给js添加具体的类型时

8. unknow

不确定类型(ts3.x才有)

unknow类型只能给any和unknow赋值

any类型可以赋值给任意类型

推荐any,因为限制了乱赋值

// 下面不知道res接受的是number还是string
const foo = () => 1
const bar = () => "hello"

let flag = true
let res: unknow

res = flag ? foo() : bar()

let msg:string = res // 如果是res是unknow则报错,因为unknow只能赋值给any或者unknow,限制了乱用

9. tuple

弥补了数组的缺点,数组中的元素不知道每个元素的类型,调取某一个元素的length可能报错;元组可以指定每个元素的类型

image.png

元组应用场景:

image.png

image.png

10. void

11. null

let n: null = null

12. undefined

let u: undefined = undefined

13. never

image.png

14. 对象类型

定义对象类型只需要把对象的属性列举出来,并给属性增加对应的类型就可以

// z是可选的
function printPoint(point: {x: number, y: number, z?:number}){}
printPoint({x: 123, y:321})
printPoint({x: 123, y:321, z:111})

对象作为参数时,也可以针对interface或者type定义的对象类型给予默认值通过解构的方式

// z是可选的
function printPoint(point: {x: number, y: number, z?:number}){}
printPoint({x: 123, y:321})
printPoint({x: 123, y:321, z:111})

// 采用type定义类型别名:
type PointType = {x: number, y: number}:
function printPoint({x: 0, y = 0}: PointType){}


// 采用interface定义类型
interface PointType{
  x: number;
  y?: number // 可选属性
}
function printPoint(point: PointType){}

readonly属性只读,无法赋值,引用类型的内容仍然可以可变

// 示例1:非引用类型
interface SomeType {
  readonly prop: string;
}

function doSomething(obj: SomeType) {
  console.log(`prop has the value '${obj.prop}'.`);
  // 报错 不可以赋值
  obj.prop = "hello";

}


// 示例2:引用类型
interface Home {
  readonly resident: { name: string; age: number };
}

function visitForBirthday(home: Home) {
  console.log(`Happy birthday ${home.resident.name}!`);
  // 内部内容可以修改
  home.resident.age++;
}

function evict(home: Home) {
  // 报错不能被重新赋值
  home.resident = {
    name: "Victor the Evictor",
    age: 42,
  };
}

15. 函数类型

  1. 常规函数表示
// 格式
function 函数名(参数名: 参数类型): 返回值类型 {
}

// 示例1:
function add(a: number, b:number):number{
    return a+b
}
  1. 箭头函数

// 示例1:指定函数类型
const foo = () => {}

// 增加函数类型注解() => void
const foo:() => void = () => {}

// 看起来复杂借助type定义类型
type MyFun = () => void
const foo:MyFun = () => {}


// 示例2:
const add: (n:number, m:number) => number = (a: number, b:number) => a+b

type addFunType = (n:number, m:number) => number

const add: addFunType = (a: number, b:number) => a+b


// 示例3:函数的参数的可选参数,必须写在最后面
const add = (a: number, b?:number) => a+b


// 示例4:函数的剩余参数
function foo(...nums: number[]){}
foo(123, 333) // ...nums可以传入任意个参数,会自动放到nums数组中
  1. 函数作为参数
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}
  1. 函数约束

让我们写一个函数,函数返回两个值中更长的那个。为此,我们需要保证传入的值有一个 number 类型的 length 属性。我们使用extends语法来约束函数参数:

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}
 // longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);

函数重载:两种实现方式,一种是通过联合类型;另一种是函数重载方式

16. 联合类型

不确定变量是哪一种类型时使用;要么是string,要么是number,传入时只能是两种中的其中一种确定类型,所以内部需要做判断

// id可以是string, 也可以是number
function printPoint(id : string | number){}

注意:如果是联合类型,代码中使用时,必须做相应的判断

function printPoint(id : string | number){
  // 必须判断是string的情况,再做string相应的操作
  // 否则当直接使用string相关的操作时,会提示警告错误,因为有可能是number,然而number不具有string的某些操作
  if(typeof id == "string"){
  }else {
    // id是number时的操作
  }
}

printPoint()

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

17. 可选属性和联合类型的关系

可选属性类似于是联合类型和undefined的联合类型

// id?: string 相当于 id:string|undefined
function printPoint(id?: string){}

18. 字面量类型

18.1 字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值

// 示例1:
// 字符串也可以作为字面量类型
const msg: "hello" = "hello"

// 用途:是需要结合联合类型使用

let align: "left" | "top" | "bottom" | "right"

align  = "top"

align  = "haha" // 错误,只能赋值指定的字面量
// 示例2:
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

18.2 数字字面量类型

19. 交叉类型

交叉类型用于合并已经存在的对象类型

  1. 交叉类型
interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}

type ColorfulCircle = Colorful & Circle;

Colorful 和 Circle 产生了一个新的类型ColorfulCircle,新类型拥有 Colorful 和 Circle 的所有成员

  1. 接口继承和交叉类型

使用继承的方式,重写属性类型会报错:

interface Colorful {
  color: string;
}

interface ColorfulSub extends Colorful {
  color: number
}

使用交叉类型的方式,重写属性类型不会报错,并且会把对应的属性的类型进行合并

interface Colorful {
  color: string;
}

type ColorfulSub = Colorful & {
  color: number
}

// 虽然不会报错,那 color 属性的类型是什么呢,答案是 never,取得是 string 和 number 的交集

类型收窄

1. typeof判断

function printPoint(id : string | number){
  // 必须判断是string的情况,再做string相应的操作
  // 否则当直接使用string相关的操作时,会提示警告错误,因为有可能是number,然而number不具有string的某些操作
  if(typeof id == "string"){
  }else {
    // id是number时的操作
  }
}

2. in判断收窄

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };

function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) {
    animal; // (parameter) animal: Fish | Human
  } else {
    animal; // (parameter) animal: Bird | Human
  }
}

3. instanceof收窄

4. 可辨别联合类型收窄

场景:计算面积(有可能是圆形、或者正方形)

interface Shape {
  kind: "circle" | "square";
  radius?: number;      //圆形时传入
  sideLength?: number;  // 正方形时传入
}

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
		// Object is possibly 'undefined'.
  }
}

此时ts会报错,因为radius是可选的,就算是circle的情况,radius也可能没传,所以直接访问shape.radius则为undefined,此时可以加非空断言,shape!.radius,可以解决ts报错,但并非完美。因为从接口的定义是可选的,但是实际操作中又认为是非空的,语义前后矛盾。

此时 Shape的问题在于类型检查器并没有方法根据 kind 属性判断 radius 和 sideLength 属性是否存在,而这点正是我们需要告诉类型检查器的,所以我们可以这样定义 Shape:

// 示例:完美解决方案
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}
type Shape = Circle | Square;

function getArea(shape: Shape) {
  // 这里还是需要判断,否则万一传入的是Square类型,则没有radius属性
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  }
}

type类型别名

有时候变量或者属性的类型比较复杂,可以采用type定义对应的类型别名

// 示例1:
function printPoint(id: string|number|boolean){
}
// 采用type定义类型别名:
type Dtype = string|number|boolean:
function printPoint(id: Dtype){}

// 示例2:
function printPoint(point: {x: number, y: number, z?:number}){}
// 采用type定义类型别名:
type PointType = {x: number, y: number, z?:number}:
function printPoint(id: PointType){}

注意:可以通过联合类型或者交叉类型来扩展type定义的类型

断言

1. 类型断言as

有的时候,你知道一个值的类型,但 TypeScript 不知道。

举个例子,如果你使用 document.getElementById,TypeScript 仅仅知道它会返回一个 HTMLElement,但是你却知道,你要获取的是一个 HTMLCanvasElement。

这时,你可以使用类型断言将其指定为一个更具体的类型:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

image.png

比如(不建议):想把string转number类型;可以先把string通过as转为any类型,再把any通过as转成number类型

const msg = "hello"
const num: number = (msg as any/unknow) as number

2. 非空断言!

funtion foo(msg?:string){
    console.log(msg.length)
}

foo("hello") // 报错,因为msg是可选的,万一不传呢,就有可能是空,那么空的length所以就会报错,
// 两种方式: 一种是直接在foo函数中增加逻辑代码if判断msg有值再打印;
// 另一种是将 msg.length 变为 msg!.length  !强制指定msg不为空

3. 确定赋值断言

1. 类定义

class Person {
    // 可以直接name,也可以name:string,也可以name:string = "why"
    name?: string // 这里必须写否则报错 类型不写默认都是any
    age: number // 这里必须写否则报错
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    eating() {
        console.log(`${this.name} eating...`);
    }

}
const p = new Person("why", 18);

p.eating();

2. 继承

// 示例1:基本继承
class Person {
    name: string = "";
    age: number = 0;
    eating() {
        console.log(`eating...`);
    }
}

class Student extends Person {
    id: number = 0;
    studying() {
        console.log("studying...");
    }
}

class Teacher extends Person {
    title: string = "";
    teaching() {
        console.log("teaching...");
    }

}

const stu = new Student();
stu.name = "why";
stu.age = 18;
stu.eating();
// 示例2:constructor
class Person {
    name: string
    age: number
    constructor(name: string, age: number){
        this.name = name
        this.age = age
    }
    eating() {
        console.log(`eating...`);
    }
}

class Student extends Person {
    id: number
    constructor(name: string, age: number, id: number){
        super(name, age)
        this.id = id
    }
    // 重写父类方法
    eating() {
        // 如果希望子类调用自己的eating时也调用父类的eating;可以使用super.eating()
        console.log(`eating...`);
    }
    studying() {
        console.log("studying...");
    }
}

class Teacher extends Person {
    title: string
    constructor(name: string, age: number, title: string){
        super(name, age)
        this.title = title
    }
    teaching() {
        console.log("teaching...");
    }

}

const stu = new Student("why", 18, 111);
stu.eating();
console.log(stu.name)
console.log(stu.age)

3. 多态

多态可以写出更加通用的代码

多态:父类引用指向子类对象

class Animal {
    action() {
        console.log("animal");
    }
}

class Dog {
    action() {
        console.log("Dog runing");
    }
}

class Fish {
    action() {
        console.log("Fish swiming");
    }
}

// 这里使用Animal类型数组接受
function mutiAction(animals: Animal[]){
    animals.forEach(animal => {
        animal.action() // runing、 swiming
    })
}

// 传递的时子类的对象,相当于父类引用指向了子类对象 形成多态
mutiAction([new Dog(), new Fish()])

// let animal:Animal = new Dog() // 父类引用指向子类对象

4. 类修饰符

类属性和方法支持3种修饰符:

  1. public(默认):任何地方可见可用
  2. protected:仅在当前类自身内部和子类中可用
  3. private:仅在当前类内部可用
class Person{
    public name: string = ""
}

// protected示例:
class Person{
    protected name: string = "123"  // protected在子类Student的getName中访问了

}
class Student extends Person{
    getName(){
        return this.name
    }
}

const stu = new Student()
console.log(stu.getName()) // 123

5. readonly

  1. 只能在构造器中可以被赋值,赋值之后就不能修改
  2. readonly属性本身不能修改,但是如果他是对象,对象中的属性是可以修改的
class Person{
    // readonly name: string = "123" // 如果在这里初始化赋值了,再外面就不能再修改了
    readonly name: string
    age?: number
    readonly friends?: Person
    constrcutor(name: string,  friends?: Person){
           this.name = name
           this.friends = friends
    }
}

const p = new Person("kobe", new Person("why"))

p.name
p.friends

// p.friends = new Person("james") 不能直接修改friends

p.friends.age = 19 // 可以修改

// p.friends.name = "xxx" // 不可以修改 name是readonly

6. setter和getter访问器(推荐)

比如想读取私有变量name,一般提供setName和getName对应的方法,通过调用对应方法进行访问;但是在ts中推荐使用setter和getter访问器进行操作

// 示例1:
class Person{
    private _name: string
    constructor(name: string){
        this._name = name
    }
    setName(name: string){
        this._name = name
    }
    getName(){
        return this._name
    }
}

const p = new Person("why")
p.setName("kobe")
p.getName()


// 示例2:setter和getter
class Person{
    private _name: string
    constructor(name: string){
        this._name = name
    }
    // setter: setter 访问器名
    set name(newName: string){
        this._name = newName
    }
    // getter
    get name(){
        return this._name
    }
}

const p = new Person("why")

// setter方式直接通过setter访问器访问
p.name = "code"

// getter方式直接通过getter访问器访问
console.log(p.name)

7. 静态属性

class Person{
    static name: string
    static fun(){
        console.log("static方法")
    }
}

Person.name
Person.fun()

8. 抽象类

一般使用类多态继承就可以,为什么还需要抽象类呢?因为大多时候在父类中公用方法,都会被子类重写,所以父类中的公用方法的方法体没有意义,此时我们可以将父类的方法定义为抽象方法,父类也会成为抽象类

c抽象类特点:

  1. 不能new
  2. 抽象方法必须被子类实现,否则子类也是抽象类
  3. 抽象方法必须包含在抽象类中
// 如果没有抽象类,没有抽象方法:
function makeArea(shape: any) {
    return shape.getArea();
}

class Rectangle {
    private width: number;
    private height: number;

    constructor(width: number, height: number) {
        this.width = width;
        this.height = height;
    }
    getArea() {
        return this.width * this.height;
    }
}

class Circle {
    private r: number;
    constructor(r: number) {
        this.r = r;
    }
    getArea() {
        return this.r * this.r * 3.14;
    }
}

console.log(makeArea(new Rectangle(10, 20)));

console.log(makeArea(new Circle(10)));

console.log(makeArea(10)); // 运行时会报错 10没有getArea()

以上10没有getArea()就会报错,我们可以通过抽象类限制makeArea的输入:

// 这里参数类型限制
function makeArea(shape: Shape) {
    return shape.getArea();
}
// 抽象类
abstract class Shape {
    // 抽象方法不能有方法体
    abstract getArea()
}

class Rectangle extends Shape{
    private width: number;
    private height: number;

    constructor(width: number, height: number) {
        super()
        this.width = width;
        this.height = height;
    }
    getArea() {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    private r: number;
    constructor(r: number) {
        super()
        this.r = r;
    }
    getArea() {
        return this.r * this.r * 3.14;
    }
}

console.log(makeArea(new Rectangle(10, 20)));

console.log(makeArea(new Circle(10)));

console.log(makeArea(10)); // 编写代码时会提示错误

接口

接口的主要作用是:1. 实现类型约束;2. 接口可以实现多继承,类只能单继承类,但是可以实现多个接口

0. 接口使用场景

场景一:类型约束

interface IEat {
    eating: () => void
}

// IEat接口是info对象的类型,那么info中就必须包含eating函数
const info: IEat = {
    eating(){
        log
    }
}

// foo函数的参数eat类型是IEat接口类型,那么传入的eat对象必须带有eating函数,符合IEat接口对eating函数的类型约束
function foo(eat: IEat){
}

场景二: 接口继承

场景三: 类实现多接口

interface IEat {
    eating: () => void
}

interface ISwim {
    swimming: () => void
}

class Animal {
}

// 单继承类,实现多接口
class Fish extends Animal implements IEat, ISwim {
    eating(){}
    swimming(){}
}

编写公共API:面向接口编程;(面向接口编程的前提是 类实现对应接口)

interface IEat {
    eating: () => void
}

interface ISwim {
    swimming: () => void
}

class Animal {}

class Fish extends Animal implements IEat, ISwim {
    eating(){}
    swimming(){}
}

class Person implements ISwim {
    swimming(){}
}

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

// 所有实现了ISwim接口的类都可以传入
swimAction(new Fish())
swimAction(new Person())

1. 接口声明

除了通过type声明类型别名之外,也可以通过interface声明类型

// 方式一:type声明对象的类型别名
type InfoType = {
    name: string,
    age: number
}
const info: InfoType = {
    name: "why",
    age: 18
}


// 方式二:interface声明对象的类型别名
interface IInfoType {
    readonly name: string,
    age: number,
    friends?: {
        name: string
    }
}
const info: IInfoType = {
    name: "why",
    age: 18,
    friends: {
        name: "kobe"
    }
}

2. 接口索引类型

// 索引类型
interface IndexLanguage {
    // 定义key必须是number类型(采用index),value是string
    [index: number]: string
}
const front: IndexLanguage= {
    0: 'html',
    1: 'css'
}

// 定义key是字符串(采用name) value是number
interface IndexLanguage {
    [name: string]: number
}
const front: IndexLanguage= {
    'html': 999
}

所以类型可以让interface实现对对象和数组的约束:

// 对数组的约束
interface IndexLanguage {
    [index: number]: string
}
const frontArr: IndexLanguage= ['c++', 'c']


// 对对象的约束
interface IndexLanguage {
    [index: string]: number
}
const frontObj: IndexLanguage= {
    'html': 999
}

3. 接口和type的区别

  1. 如果是定义非对象类型,推荐type
  2. 如果是对象类型
    • interface可以重复定义的接口,接口中的不同属性,接口的属性会合并
    • type不支持重复定义别名
// 示例1:
interface Foo{
    name:string
}

interface Foo{
    age:number
}

// 最终的Foo接口中会有两个存在两个属性,分别是name和age

// 示例2:
type Foo = {
  name: string
}

type Foo = {
  age: number
}
// type重复定义Foo报错

备注:

1、interface:表示用于描述对象的结构和属性;(interface可以被实现(implements)或者扩展(extends))

2、type: 表示类型的别名;(允许为任何类型创建别名——典型的是进行类型的复杂操作)

3、注意两者的区别:type是类型的别名,为了进行类型的交叉联合产生新类型的等;但是interface不是作用于类型,而是为了描述对象的结构和属性

4、interface重名会合并属性,type不能重名;

5、interface可以被类实现,type不能

6、type支持联合类型和交叉类型,interface不支持

3.1 对象和函数

// 示例1:对象描述
type InfoType = {
    name: string,
    age: number
}
// 这里和type不一样,这里没有赋值等号
interface InfoType {
    name: string,
    age: number,
}

const info: InfoType = {
    name: "why",
    age: 18
}


// 示例2:函数描述

type SetPointType = (x: number, y: number) => number; // 这里是箭头符号
interface SetPointType {
     (x: number, y: number) : number; // 这里不是箭头符号 => 而是冒号
}
const SetPoint: SetPointType = (x: number, y: number) => x+y

4. 接口继承

接口可以实现多继承

interface ISwim {
    swimming: () => void
}

interface IFly {
    flying: () => void
}

interface IAction extends ISwim, IFly {
}

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

注意:多继承除了interface之外,还可以采用交叉类型实现


5. 接口的实现

泛型

函数调用可以自动类型推导;但是对象就不行,比如第三点

1.认识泛型

泛型本质:类型参数化(就是具体的类型通过参数的方式在调用时传入进来)

function sum<T>(num: T): T{
    return num
}
// 1. 调用方式1:明确传入类型
sum<number>(20)
sum<{name : string}>({name: "why"})
sum<any[]>(["abc"])

// 2. 调用方式2:类型推导
sum(30)

2. 泛型多类型

function sum<T, K>(num: T, age: K): T{
    return num
}

sum<number, string>(20, "why")

3. 泛型接口使用

// interface IPerson<T1 = string, T2 = number>
interface IPerson<T1, T2> {
    name: T1
    age: T2
}

const p: IPerson<string, number> = {
    name: "why",
    age: 18
}

4. 泛型类的使用

class Point<T> {
    x: T
    y: T
    constructor(x: T, y: T) {
        this.x = x
        this.y = y
    }
}

// 类调用的类型传入位置和接口泛型传入的位置
const p = new Point<string>("11", "22")  // 推荐

// 也可以像下面一样指定类型传入位置
const p: Point<string> = new Point("11", "22")

5. 泛型的类型约束

// 这种联合类型穷举太繁琐
function foo(arg: string | number | {length: number} | any[]){
    return arg.length
}

以上联合类型限制参数类型传入的方式太繁琐;我们可以通过泛型,但是泛型T过于宽泛,可以传入任意类型,如何限制联合类型穷举出来的类型呢?借助泛型的类型继承进行类型的限制约束

// 示例1: 泛型T过于宽泛,什么都可以传入,达不到类型约束(只需要部分指定类型)
function foo<T>(arg: T){
    return arg.length
}


// 示例2:
interface ILength {
    length: number
}

// T extends ILength: 表示T是ILength的子类,所以外界传入的T类型必须有length属性;否则报错
function foo<T extends ILength>(arg: T){
    return arg.length
}

foo(123) // 会报错
foo([1,2,3]) // 正确
foo("why") // 正确
foo({length : 100}) // 正确

8. 泛型应用理解

8.1 常规函数泛型

// 示例1:常规返回泛型
// identity<T,R> 函数后的T和R必须传递,不然参数和返回值包括函数内部都不能使用T和R
// 函数后接受的T和R,会传递给参数的T和R;接着再传递给返回值的T
function identity<T,R>(arg: T): T {
    let age:R
    log(age,arg)
    return arg;
}

8.2 类中使用

类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

// 示例1: 通过类传入T,可以在属性和方法的参数和返回值上直接使用
class Person<T>{
    name:T
    constructor(name: T){
        this.name = name
    }
    setName(name: T){
        this.name = name
    }
}
const p = new Person<string>("why")

// 示例2:直接在类方法中传入T,方法中 setName<T>(name: T)的T只作用该方法的参数和返回值以及方法内部(和普通函数传入T效果一样),调用setName时在调用处传递T
class Person{
    name: any
    constructor(name: any){
        this.name = name
    }
    setName<T>(name: T){
        this.name = name
    }
}


const p = new Person("why")
p.setName<number>(110)


// 示例3: 当类和方法都传入泛型T时,方法的T会生效

8.3 在接口中使用T

// 示例2:接口中定义函数: 留意接口中定义的函数和类方法的T一样,也不一定需要接口传入T
interface GenericIdentityFn {
    name: T  // 也可以写  但是没有意义
    <T>(arg: T): T;
}

function identity<T>(arg: T): T { return arg; }

let myIdentity: GenericIdentityFn = identity;

myIdentity<string>("why")



// 示例3:接口定义函数传入T (留意如果接口传入了T,方法类型前要传入的T就没有了)
interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T { return arg; }

let myIdentity: GenericIdentityFn<string> = identity;

identity("why") 或者 identity<string>("why")

identity<number>(111) // 也可以;以方法的为准,打印111, 并且是number类型

8.4 泛型定义类型以及应用场景

// 示例1:函数类型简写
function identity<T>(arg: T): T { return arg; }

方式1let myIdentity: <U>(arg: U) => U = identity;

方式2let myIdentity: {<T>(arg: T): T} = identity;

<U>(arg: U) => U // 表示的是identity函数的函数类型简写;到时候传入的U
// {<T>(arg: T): T} 和 <U>(arg: U) => U 的写法一样


// 示例2:接口中定义函数: 留意接口中定义的函数和类方法的T一样,也不一定需要接口传入T
interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T { return arg; }
let myIdentity: GenericIdentityFn = identity;
myIdentity<string>("why")


// 示例3:接口定义函数传入T (留意如果接口传入了T,方法类型前要传入的T就没有了)
interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T { return arg; }

let myIdentity: GenericIdentityFn<string> = identity;

identity("why") 或者 identity<string>("why")

identity<number>(111) // 也可以;以方法的为准,打印111, 并且是number类型


应用场景:

对原有的类型支持的属性做进一步扩展,可以让类型支持的属性约束更强大:

function loggin<T>(arg: T): T
{ console.log(arg.length); // Error: T doesn't have .length return arg; }

由于可能传入number,number没有length属性,所以我们可以扩展原有T类型,再不改变的T类型的前提下,让T通过extends其他包含length属性接口R,已达到最终传入的T类型必须包含除了T接口本身的属性之外,还必须包含R类型中的属性。

interface R {
    length:number
}
// 最终这里的T包含了原本的T和R所有的属性
function loggin<T extends R>(arg: T): T
{
    console.log(arg.length);
}

logging(3) // 会报错
logging({length: 10, name:"why"})

模块化和命名空间

模块化: 支持esModule和commonJS

命名空间(namespace): 最早称之为内部模块,主要目的是将一个模块内部再进行作用域的划分,防止一些命名冲突的问题(比如在一个文件中两个函数名冲突的场景)

// bar.ts: 在一个文件中两个函数名冲突
function format(){
     return 10
}

function format(){
     return 99
}

使用namespace可以解决一个文件中两个函数名冲突的场景

// 示例1:bar.ts
namespace time{
    // 要想在time的namespace外部访问到,则需要export导出
    export function format(){
        return 10
    }
}

namespace price{
    export function format(){
        return 10
    }
}

// 示例2:bar.ts文件内部使用
time.format()
price.format()

// 示例3:bar.ts文件外部使用,则需要把namespace导出

export namespace time{
    // 要想在time的namespace外部访问到,则需要export导出
    export function format(){
        return 10
    }
}

import { time, price } './bar.ts'
time.format()
price.format()

类型的查找规则以及声明

为什么在文件中axios可以使用,原生的domAPI可以使用,但是lodash不能用

ts中有两种文件:

  1. .ts文件: 写代码的地方,最终这个ts文件会输出js文件
  2. .d.ts文件:用来做类型的声明(declare),仅仅用做类型检测,告知typescript我们有哪些类型

ts在3个地方找类型声明:

  1. 内置类型声明
  2. 外部定义类型声明
  3. 自己定义类型声明

内置类型声明:

比如在ts文件中可以直接使用document.getElementById, 也能识别到HTMLElement类型等,是因为typescript内置了DOM的类型;在node_modules/typescript/lib/lib.dom.d.ts中声明。

内置类型是ts内置的, 比如Math、Data、DOM API、Window、Document等


外部类型声明:

axios库:在node_modules/axios/index.d.ts中批量声明了类型,所以在业务文件中,可以直接import导入后直接使用axios

lodash库:在业务文件中导入后不能使用,因为没有提供xxx.d.ts声明文件

外部类型声明有两种方式:

  1. 一种是在自己库里面集成了.d.ts类型声明(axios)
  2. 一种是通过社区的一个公有库DefinitelyTyped存放类型声明文件(这种方式是库和类型声明文件不放在一起;类型声明文件查找网址:www.typescriptlang.org/dt/search?s…

image.png


自定义声明类型

如果库没有自己集成d.ts声明文件,并且在typescript提供types(DefinitelyTyped)平台也没有提供对应的类型声明文件,则需要自己开发手动编写.

// 创建lodash.d.ts文件,并在文件中手写声明

declare module 'lodash' {
    export function join(arr: any[]): void
}

// main.ts
import lodash from 'lodash'

lodash.join(["a", "b"]) // 编译成功

类型声明文件:可以声明变量/函数/类

在index.html中的script标签中写ts代码,在main.ts中使用,但是编译报错,为什么呢?平时编写的ts代码经过编译打包之后变成js代码,最终也会被导入到index.html中,那为什么在index.html的script标签中编写的ts代码在main.ts中不能用呢?编译会报错,这是ts的规范,需要有类型声明,此时可以在.d.ts文件中进行声明,然后再编译打包就可以了

// index.html
<scrpit>
    let whyName = "why"
    let whyAge = 18
    function whyFoo(){
        console.log("whyFoo")
    }
</scrpit>

// main.ts
log(whyName)
log(whyAge)
whyFoo()


// xxx.d.ts
declare let whyName: string
declare let whyAge: number
declare function whyFoo(): void

// 然后编译打包就可以了

// xxx.d.ts

// 1. 声明模块
declare module 'lodash' {
    export function join(arr: any[]): void
}

// 2. 声明变量/函数/类
declare let whyName: string
declare let whyAge: number
declare function whyFoo(): void
declare class Person {
    name: string
    constructor(name: string)
}

// 3. 声明一个文件:比如说import图片

declare module "*.jpg"  // 表达的意思是可以将jpg图片以文件的形式import导入
declare module "*.png"

关键字

1. is运算符

语法:parameterName is T:parameterName的类型如果是T类型则返回true,否则返回false;(parameterName必须是当前函数的参数名)

如果像下面这样写已经比较简洁了,但是调用方法的时候,还是要进行类型转换才可以,否则还是会报错

function isBird(bird: Bird | Fish): boolean {
    return !!(bird as Bird).fly;
}

function isFish(fish: Bird | Fish): boolean {
    return !!(fish as Fish).swim;
}

function start(pet: Bird | Fish) {
    // 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();

    if (isBird(pet)) {
        (pet as Bird).fly(); // 这里在调用方法时仍然需要类型转换,否则报错
    } else if (isFish(pet)) {
        (pet as Fish).swim();
    }
}

is此时可以派上用场,可以让我们判断完类型之后,就可以直接调用方法,不用再进行类型转换


// 留意看is在这里的用法:如果bird is Bird: 如果bird是Bird类型返回true
function isBird(bird: Bird | Fish): bird is Bird {
    return !!(bird as Bird).fly
}

function start(pet: Bird | Fish) {
    // 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
    pet.layEggs();

    if (isBird(pet)) { // 这里返回true说明告知了ts,此时的pet就是Bird类型了;所以if的内部就不需要再进行类型转换才可以调用
        pet.fly(); // 这里不需要再进行类型转换
    } else {
        pet.swim();
    }
};

2. in运算符

语法:key in 联合类型K:操作符作用遍历类型;

// 示例1:
type U = 'a'|'b'|'c';

type Foo = {
  [Prop in U]: number;
};
// 等同于
type Foo = {
  a: number,
  b: number,
  c: number
};

// 示例2:
type NewProps<Obj> = {
  [Prop in keyof Obj]: boolean;
};
// 用法
type MyObj = { foo: number; };
// 等于 { foo: boolean; }
type NewObj = NewProps<MyObj>;

[Prop in U]表示依次取出联合类型U的每一个成员。

[Prop in keyof Obj]表示取出对象Obj对象类型的每一个键名。



// 示例2:
type roles = "tester" | "developer" | "manager";
const staffCount: { [k in roles]: number } = {
  tester: 100,
  developer: 200,
  manager: 300,
};

上述代码规定 staffCount 是一个对象,属性名为 roles 约束的三个,值为 number 类型;类型变量 k,以此绑定到对象的每一个属性,遍历三个字符串字面量组成的联合类型 roles,number 为每个属性的值的类型。



in运算符的另一个用法是:可以做新旧类型之间的映射

// 示例3:
interface publicObj {
  // 定义一个开放的对象
  name: string;
  age: number;
}

type ReadonlyObj<T> = { // 需要传递一个类型参数
  readonly [K in keyof T]: T[K]; // keyof T 返回联合类型 in 再遍历该联合类型
};
// 使用
let obj: ReadonlyObj<publicObj> = {
  name: "myName",
  age: 6,
};
obj.name = "yourName"; // 无法分配到 "name" ,因为它是只读属性。ts(2540)


3. extends

注意这里的extends不是类接口的继承

语法:T extends K:表示T类型是K类型的子类型(可以用作类型收窄);

让我们写一个函数,函数返回两个值中更长的那个。为此,我们需要保证传入的值有一个 number 类型的 length 属性。我们使用extends语法来约束函数参数:

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}
 // longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);

4. typeof

语法:"typeof x":表示变量x的类型

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型

interface Person {
    name: string; age: number;
}
const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person

function toArray(x: number): Array<number> {
    return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

5. keyof操作符

语法:"keyof T":表示获取T类型中的所有键名组成的联合类型

keyof 该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型

// 示例1:keyof用于interface
interface Person {
  name: string;
  age: number;
  location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string | number


// 示例2:keyof应用class
class Person {
  name: string = "Semlinker";
}

let sname: keyof Person;
sname = "name";

// 示例3:keyof引用基本类型
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"

keyof应用场景:

// 示例1:
// ts中会报错:元素隐式地拥有 any 类型,因为 string 类型不能被用于索引 {} 类型

// 报错原因:key的类型并不是string,在js中默认帮助转为string,但ts默认不会帮转,所以认为key不是string类型

for (const key in obejct) {
	obejct[key]
}


// 修复:

export function isValidKey(
    key: string | number | symbol,
    object: object
): key is keyof typeof object {
    return key in object;
}

for (const key in obejct) {
  if(isValidKey(key, obejct)){
    	obejct[key]
  }
}

// 解析:typeof object 会获取到object这个对象的类型X,然后keyof X就会获取到X类型中的所有属性名的联合
// 示例2:同示例1报错
function prop(obj: object, key: string) {
  return (obj as any)[key];
}

// 修复:
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

在以上代码中,我们使用了 TypeScript 的泛型和泛型约束。首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型

6. instanceof

7. T[key]方括号索引运算符

T[key]语法含义:类似于 js 中使用对象索引的方式,只不过 js 中是返回对象属性的值,而在 ts 中返回的是 T 对应属性 key 的类型。

interface Person {
    name: string
    age: number
    weight: number | string
    gender: 'man' | 'women'
}

type NameType = Person['name']  // string
type WeightType = Person['weight']  // string | number

运算符高级类型和技巧

1. Omit<Type, Keys>: 排除属性

语法:Omit<Type, Keys>:表示从 Type 类型中排除部分属性键名keys,将剔除keys属性之后剩下的属性组成的对象作为新的对象类型返回。(备注:指定的键名Keys必须是对象键名Type里面已经存在的键名,否则会报错)

// Omit<Type, Keys>的实现方式:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 示例1:
interface Todo {
    title: string;
    description: string;
    completed: boolean;
    createdAt: number;
}
type TodoPreview = Omit<Todo, "description">;

// TodoPreview类型中就只有title、completed、createdAt
const todo: TodoPreview = {
    title: "Clean room",
    completed: false,
    createdAt: 1615544252770,
};


// 示例2:
type TodoInfo = Omit<Todo, "completed" | "createdAt">;

// TodoInfo中只剩下title、description
const todoInfo: TodoInfo = {:
    title: "Pick up kids",
    description: "Kindergarten closes at 5pm",
};

// 示例3:
interface A {
  x: number;
  y: number;
}
// 上面示例中,对象类型A中不存在属性z,所以就原样返回了。
type T = Omit<A, 'z'>; // { x: number; y: number }



使用场景:

比如需要继承某个类型,并且重写对应的属性类型时(不能直接使用接口继承,因为overwrite,因为会报错)。

interface Colorful {
  color: string;
}

interface ColorfulSub extends Colorful {
  color: number
}

// 会报错

这时候可以借用Omit实现继承类型并且重写对应的属性类型 (本质是通过Omit把要重写的属性过滤,然后再和要重写的属性进行交叉运算生成新类型)

// 示例1:重写Todo的description
interface Todo {
    title: string;
    description: string;
}

type MyTodo = Omit<Todo, 'description'> & {description: () => void}

// 相当于
type MyTodo = {
   title: string,
   description: () => void
}

2. Pick<Type, Keys>:提取属性

语法:Pick<Type, Keys>:表示从 T 类型中提取部分属性键名keys,作为新的对象类型返回。(备注:指定的键名Keys必须是对象键名Type里面已经存在的键名,否则会报错)

// Pick<Type, Keys>的实现方式:
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}


// 示例1:
interface A {
  x: number;
  y: number;
}

type T1 = Pick<A, 'x'>; // { x: number }
type T3 = Pick<A, 'x'|'y'>;  // { x: number; y: number }

使用场景:比如发送网络请求时,只需要传递类型中的部分参数,此时可以借助Pick实现:

interface Goods {
    type: string
    goodsName: string
    price: number
}

// 作为网络请求参数,只需要 goodsName 和 price 就可以
type RequestGoodsParams = Pick<Goods, 'goodsName' | 'price'>
// 返回类型:
// type RequestGoodsParams = {
//     goodsName: string;
//     price: number;
// }
const params: RequestGoodsParams = {
    goodsName: '',
    price: 10
}

3. Exclude<UnionType, ExcludedMembers>: 从联合类型U中剔除或者排除成员类型

语法:Exclude<U, E> :从联合类型U中剔除成员类型之后剩下的类型作为新的联合类型返回。

// Exclude<U, E>实现方式:

type Exclude<T, U> = T extends U ? never : T;


type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Exclude<string | number | (() => void), Function>;  // string | number

4. Extract<UnionType, Union>:从联合类型UnionType中提取指定类型Union

语法:Extract<UnionType, Union>用来从联合类型UnionType之中,提取指定类型Union,组成一个新类型返回;

// Extract<UnionType, Union>的实现:
type Extract<T, U> = T extends U ? T : never;


// 示例1:不包含则返回never
type T = Extract<string|number, boolean>; // never

// 示例2:
type T1 = Extract<'a'|'b'|'c', 'a'>; // 'a'

5. Required:必选属性

语法:Required:表示将T类型中的所有属性变为必选属性

Required的实现方式:用于将 T 类型的所有属性设置为必选状态,首先通过 keyof T,取出类型 T 的所有属性,然后通过 in 操作符进行遍历,最后在属性后的 ? 前加上 -,将属性变为必选属性。

// Required<T>实现方式:
type Required<T> = {
    [P in keyof T]-?: T[P];
}


interface Person {
    name?: string
    age?: number
}

// 使用 Required 映射后返回的新类型,name 和 age 都变成了必选属性
// 会报错:Type '{}' is missing the following properties from type 'Required<Person>': name, age
let person: Required<Person> = {}

6. Partial:可选属性

语法:Partial : 表示将T类型中的属性全部转为可选属性

Partial的实现方式:用于将 T 类型的所有属性设置为可选状态,首先通过 keyof T,取出类型 T 的所有属性,然后通过 in 操作符进行遍历,最后在属性后加上 ?,将属性变为可选属性。

// Partial<T>的实现方式
type Partial<T> = {
  [P in keyof T]?: T[P]
}


// 示例1:
interface Person {
    name: string
    age: number
}

// 会报错:Type '{}' is missing the following properties from type 'Person': name, age
// let person: Person = {}

// 使用 Partial 映射后返回的新类型,name 和 age 都变成了可选属性
let person: Partial<Person> = {}

person = { name: 'pengzu', age: 800 }
person = { name: 'z' }
person = { age: 18 }



// 示例2:
export interface IData {
    name: string;
    age: number;
}
setHelpCenter(partial: Partial<IData>) {
  this.$patch(partial);
}

Partial<IData>会将IData类型中的属性全部转为可选属性,相当于下面:

{
    name?: string;
    age?: number;
}

7. Parameters

语法:Parameters:从函数类型Type里面提取参数类型,组成一个元组返回(备注:type是函数类型)

// Parameters<Type>实现方式:
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P)
  => any ? P : never
// 示例1:
type FunctionType = (name: string, age: number, test: string) => boolean

type FunctionParamsType = Parameters<FunctionType>  // [name: string, age: number, test: string]

const params:  FunctionParamsType = ['Jack', 20, 'hello']


// 示例2:
type T1 = Parameters<string>;// 报错
type T2 = Parameters<(s:string) => void>; // [s:string]
type T3 = Parameters<<T>(arg: T) => T>;    // [arg: unknown]

8. ReturnType

语法:ReturnType:提取函数类型Type返回值类型,作为一个新类型返回

// 示例1:
type T1 = ReturnType<() => string>; // string

type T2 = ReturnType<() => {
  a: string; b: number
}>; // { a: string; b: number }

type T3 = ReturnType<(s:string) => void>; // void

9. Record<Keys, Type>

语法:Record<Keys, Type>:返回一个对象类型,参数Keys用作键名,参数Type用作键值类型。

// Record<Keys, Type>实现如下:
type Record<K extends string|number|symbol, T>
  = { [P in K]: T; }


// 示例1:
// { a: number, b: number }
type T = Record<'a'|'b', number>;

// 示例2:
// { a: number|string }
type T = Record<'a', number|string>;


// 示例3:
type petsGroup = 'dog' | 'cat' | 'fish';
interface IPetInfo {
    name:string,
    age:number,
}

type IPets = Record<petsGroup | 'otherAnamial', IPetInfo>;

const animalsInfo:IPets = {
    dog:{
        name:'dogName',
        age:2
    },
    cat:{
        name:'catName',
        age:3
    },
    fish:{
        name:'fishName',
        age:5
    },
    otherAnamial:{
        name:'otherAnamialName',
        age:10
    }
}

10. InstanceType: 获取构造函数的返回值类型

语法:InstanceType :获取类型T的构造函数的返回类型。

// InstanceType<T>的实现方式:
type InstanceType<
  T extends abstract new (...args:any) => any
> = T extends abstract new (...args: any) => infer R ? R :
  any;


// 示例1:
class Person {
    name: string
    age: number
    weight: number
    gender: 'man' | 'women'

    constructor(name: string, age: number, gender: 'man' | 'women') {
        this.name = name
        this.age = age;
        this.gender = gender
    }
}

type Instance = InstanceType<typeof Person>  // Person

const params: Instance = {
    name: 'Jack',
    age: 20,
    weight: 120,
    gender: 'man'
}


// 示例2:实战演练
<BaseInfo
  ref="baseInfoRef"
  :businessRowId="!!businessRowId"
  :currentBusinessInfoRow="businessInfoRecord"
/>
// typeof获取模板BaseInfo的构造函数类型X, InstanceType再获取X这个构造函数的实例类型也就是构造函数的返回值类型
const baseInfoRef = ref<InstanceType<typeof BaseInfo>>();
await baseInfoRef?.value?.validateBaseInfoForm();

字符串类型工具

1. Uppercase

语法:Uppercase:将字符串类型的每个字符转为大写。

type A = 'hello';
// "HELLO"
type B = Uppercase<A>;

2. Lowercase

语法:Lowercase: 将字符串的每个字符转为小写。

type A = 'HELLO';
// "hello"
type B = Lowercase<A>;

3. Capitalize

语法:Capitalize将字符串的第一个字符转为大写。

type A = 'hello';
// "Hello"
type B = Capitalize<A>;

4. Uncapitalize

语法:Uncapitalize:将字符串的第一个字符转为小写。

type A = 'HELLO';
// "hELLO"
type B = Uncapitalize<A>;

类型映射实战

1. 类型映射

下面这两个类型的属性结构是一样的,但是属性的类型不一样;如果属性数量多的话,逐个写起来就很麻烦。

type A = {
  foo: number;
  bar: number;
};

type B = {
  foo: string;
  bar: string;
};

使用类型映射可以从A类型映射为B类型:

type A = {
  foo: number;
  bar: number;
};

type B = {
  [prop in keyof A]: string
};

上述[prop in keyof A]是一个属性名表达式,表示这里的属性名需要计算得到;具体的计算规则如下:

  • prop:属性名变量,名字可以随便起。
  • in:运算符,用来取出右侧的联合类型的每一个成员。
  • keyof A:返回类型A的每一个属性名,组成一个联合类型。

也可以B类型复制A的类型或者传入泛型类型

// 示例:
type A = {
  foo: number;
  bar: string;
};

// 复制A的属性和属性的类型
type B = {
  [prop in keyof A]: A[prop];
};

// 使用泛型传入: 可以将其他对象的所有属性值都改成 boolean 类型。
type ToBoolean<Type> = {
  [Property in keyof Type]: boolean;
};

2. 索引类型的映射

// 示例1:
type MyObj = {
  [P in 0|1|2]: string;
};
// 等同于
type MyObj = {
  0: string;
  1: string;
  2: string;
};

// 示例2:
type MyObj = {
  [p in 'foo']: number;
};
// 等同于
type MyObj = {
  foo: number;
};


// 示例3:[p in string]就是属性名索引形式[p: string]的映射写法
type MyObj = {
  [p in string]: boolean;
};
// 等同于
type MyObj = {
  [p: string]: boolean;
};


// 示例4:映射为可选
type A = {
  a: string;
  b: number;
};
type B = {
  [Prop in keyof A]?: A[Prop];
};

3. 映射修饰符

映射会原样复制原始对象的可选属性和只读属性

type A = {
  a?: string;
  readonly b: number;
};
type B = {
  [Prop in keyof A]: A[Prop];
};

// 等同于
type B = {
  a?: string;
  readonly b: number;
};

上面示例中,类型B是类型A的映射,把A的可选属性和只读属性都保留下来。

如果要删改可选和只读这两个特性,并不是很方便。为了解决这个问题,TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的?修饰符和readonly修饰符。

  • +修饰符:写成+?或+readonly,为映射属性添加?修饰符或readonly修饰符。
  • –修饰符:写成-?或-readonly,为映射属性移除?修饰符或readonly修饰符。

备注:+?或-?要写在属性名的后面;+readonly和-readonly要写在属性名的前面;+?修饰符可以简写成?,+readonly修饰符可以简写成readonly

// 示例1:
// 添加可选属性
type Optional<Type> = {
  [Prop in keyof Type]+?: Type[Prop];
};:
// 移除可选属性
type Concrete<Type> = {
  [Prop in keyof Type]-?: Type[Prop];
};


// 示例2:
// 添加 readonly
type CreateImmutable<Type> = {
  +readonly [Prop in keyof Type]: Type[Prop];
};
// 移除 readonly
type CreateMutable<Type> = {
  -readonly [Prop in keyof Type]: Type[Prop];
};

// 示例3:
type A<T> = {
  +readonly [P in keyof T]+?: T[P];
};
// 等同于
type A<T> = {
  readonly [P in keyof T]?: T[P];
};

4. 键名重映射

TypeScript 4.1 引入了键名重映射(key remapping),允许改变键名。

键名重映射的语法是在键名映射的后面加上 as + 新类型子句。这里的“新类型”通常是一个模板字符串,里面可以对原始键名进行各种操作

// 示例1:
type A = {
  foo: number;
  bar: number;
};
type B = {
  [p in keyof A as `${p}ID`]: number;
};

// 等同于
type B = {
  fooID: number;
  barID: number;
};


下面示例解析:get${Capitalize<string & P>},下面是各个部分的解释。

  • get:为键名添加的前缀。
  • Capitalize:一个原生的工具泛型,用来将T的首字母变成大写。
  • string & P:一个交叉类型,其中的P是 keyof 运算符返回的键名联合类型string|number|symbol,但是Capitalize只能接受字符串作为类型参数,因此string & P只返回P的字符串属性名。
// 示例2:
interface Person {
  name: string;
  age: number;
  location: string;
}

type Getters<T> = {
  [P in keyof T
    as `get${Capitalize<string & P>}`]: () => T[P];
};

type LazyPerson = Getters<Person>;
// 等同于
type LazyPerson = {
  getName: () => string;
  getAge: () => number;
  getLocation: () => string;
}

5. 属性过滤

键名重映射还可以过滤掉某些属性;比如下面这种它的键名重映射as T[K] extends string ? K : never],使用了条件运算符。如果属性值T[K]的类型是字符串,那么属性名不变,否则属性名类型改为never,即这个属性名不存在。这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性

type User = {
  name: string,
  age: number
}
type Filter<T> = {
  [K in keyof T
    as T[K] extends string ? K : never]: string
}
type FilteredUser = Filter<User> // { name: string }

6. 联合类型映射

wangdoc.com/typescript/…

参考系列

www.typescriptlang.org/docs/handbo…

ts.yayujs.com/handbook/Ke…

semlinker.com/ts-keyof/

blog.csdn.net/zy21131437/…

juejin.cn/post/684490…

juejin.cn/post/687211…

juejin.cn/post/698529…

wangdoc.com/typescript/…