TypeScript相关题

457 阅读14分钟

1.const func = (a, b) => a + b,要求编写Typescript,要求a,b参数类型一致,都为number或者都为string。

// 实现1:函数重载
function func(a: string, b: string): string;

function func(a: number, b: number): number;

function func(a: any, b: any): any {
    return a + b;
}

// 实现2:函数变量
interface Add {
    (a: string, b: string): string;
    (a: number, b: number): number;
}

const add: Add = (a, b) => a + b

2. 基于已有类型生成新类型:剔除类型中的width属性

ts内置工具类型Omit<T, K>,它能够从已有对象类型中剔除给定的属性,然后构建出一个新的对象类型。

interface A {
    content: string;
    width: number;
    height: number;
}
// ts内置工具类型
type NewA = Omit<A, 'width'> // 从类型A中剔除属性width

const a: NewA = {content: 'content', height: 2}

3. 基于ts静态方法和静态属性实现单例模式

构建单例设计模式

  1. 将构造器设置为私有的,不允许外部实例化
  2. 至少提供一个外部访问的方法或属性,外部可以通过这个方法或属性来得到唯一的实例,所以应该把这个方法设置为静态方法
  3. 外部调用第二步提供的静态方法来获取一个对象

1. 懒汉模式

// 懒汉式单例模式:需要使用时才创建实例,按需创建
export default class MyLocalStorage {
    static localstorage: MyLocalStorage; // 静态属性
    // 使用private访问修饰符,让类外部不可以实例化
    private constructor() {
       console.log('执行了constructor')
    }
    // 静态方法不可以访问实例属性
    public static getInstance() {
       if (!this.localstorage) {
          this.localstorage = new MyLocalStorage();
       }
       return this.localstorage;
    }

    public setItem(key: string, value: any) {
        localStorage.setItem(key, JSON.stringify(value))
    }

    public getItem(key: string) {
       const value = localStorage.getItem(key)
       return value == null ? null : JSON.parse(value)
    }
}

// MyLocalStorage.prototype.kk = function(){} // ts 不允许在原型上新增方法
// MyLocalStorage.prototype.setItem = function(){} // 但是可以修改

2. 饿汉式单例模式

// 饿汉式单例模式:立即执行, 外部通过类名获取静态属性
export default class MyLocalStorage {
    public static localstorage: MyLocalStorage = new MyLocalStorage(); // 静态属性
    private constructor() {
       console.log('执行了constructor')
    }

    public setItem(key: string, value: any) {
        localStorage.setItem(key, JSON.stringify(value))
    }

    public getItem(key: string) {
       const value = localStorage.getItem(key)
       return value == null ? null : JSON.parse(value)
    }
}

4. ts方法重载和构造器重载

1. 封装ArrayList学习方法重载

class ArrayList {
    constructor(public element: Array<object>) { } 
    // 根据索引来查询数组中指定元素
    get(index: number) {
       return this.element[index]
    }

    show() {
        this.element.forEach(item => {
            console.log(item)
        })
    }
    // 方法重载应用
    // 重载签名两个及两个以上才有意义
    remove(value: number): number; // 重载签名1(通过下标删除元素并返回删除下标)
    remove(value: object): object; // 重载签名2 (通过元素删除并返回删除元素)
    remove(value: number | object) { // 重载实现,有且仅有一个
       this.element = this.element.filter((ele, index) => {
            if (typeof value === 'number') {
                return index !== value
            } else {
                return ele !== value
            }       
       })
       return value;
    }
}
let s1 = {name:'l', age:11}
let s2 = {name:'o', age:90}
let s3 = {name:'m', age:23}
let arr = new ArrayList([s1, s2, s3]);


console.log('delete:', arr.remove(0))
console.log('delete:', arr.remove(s2))
arr.show()

2. 构造器重载

type ChartParam = {
    width?: number,
    height?: number,
    radius?: number
}
class Square {
    public width: number;
    public height: number;

    constructor(width: number, height: number) // 重载签名
    constructor(value: ChartParam) // 重载签名
    constructor(valueOrWidth: any, height: number = 0) { // // 重载实现
    // constructor(valueOrWidth: any, height?: number) {
        if (typeof valueOrWidth === 'object') {
          this.width = valueOrWidth.width;
          this.height = valueOrWidth.height
        } else {
            this.width = valueOrWidth;
            this.height = height;
        }    
    }
    
    getArea(): number {
        return this.width * this.height
    }
}

let square = new Square(40, 50)
let param: ChartParam = {width: 20, height: 10}
let s2 = new Square(param)
// console.log(square.getArea())
console.log(s2.getArea())

5. ts类型断言的9种使用场景

1. A、B都是类且具有继承关系

A、B都是类且具有继承关系,无论二者间谁继承谁,均可相互转换。

class People {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    sayHi() {
        console.log('Hi')
    }
}

class Student extends People {
    grade: number;
    constructor(name: string, age: number, grade: number) {
        super(name, age)
        this.grade = grade;
    }

    getGrade() {
        console.log(this.grade)
    }
}

let people = new People('zz', 12);

let res = people as Student; // 类型断言
let res3 = <Student>people; // 类型转换


let stu = new Student('LL', 13, 9)

let res2 = stu as People; // 类型断言:子类转换成父类,比较少用

2. A、B都是类但没有继承关系

两个类中的任意一个类的所有public的实例属性和方法(非静态属性)与另一个类的所有public的实例属性和方法相同或是子集,那么这两个类型可以相互断言。

// 两个类中任意一个类的所有public的实例属性和方法(非静态属性方法)与另一个类的
// 所有public的实例属性和方法相同或为子集,则这两个类可以相互断言
class People {
    public name: string;
    public age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

class Student {
    public name: string;
    public age: number;
    public grade: number;
    constructor(name: string, age: number, grade: number) {
        this.name = name;
        this.age = age;
        this.grade = grade;
    }

    public getGrade() {
        console.log(this.grade)
    }
}

let people = new People('zz', 12);

let res = people as Student; // 类型断言
let res3 = <Student>people; // 类型转换


let stu = new Student('LL', 13, 9)

let res2 = stu as People; // 类型断言

3. 如果A是类,B是接口,且A类实现了B接口

这种情况下,A类型的对象变量可以断言成B接口类型,同时B接口类型的对象变量也可以断言成A类型。

interface People {
    username: string;
    age: number;
    address: string;
    tel: string;
}

class Student implements People {
    public username!: string;
    public age!: number;
    public address!: string;
    public tel!: string;
    constructor(username: string, age: number, address: string) {
       this.username = username;
       this.age = age;
       this.address = address;
    }
    public getAddress() {}
}

// 定义People接口类型对象
let p: People = {
    username: 'll',
    age: 12,
    address:'SH',
    tel: '123456666677'
}
let res = p as Student; // 类型断言
let res2 = <Student>p; // 类型转换

let stu = new Student('lily', 23, 'BJ')
stu as People;// 类型断言

4. 如果A是类,B是接口,但A类没有实现B接口

断言关系和场景2规则相同,重点理解属性和方法相同或是子集关系,就可以相互断言。

// People的属性和方法是Student属性方法的子集
interface People {
    username: string;
    age: number;
    address: string;
    tel: string;
}

class Student{
    public username!: string;
    public age!: number;
    public address!: string;
    public tel!: string;
    constructor(username: string, age: number, address: string) {
       this.username = username;
       this.age = age;
       this.address = address;
    }
    public getAddress() {}
}

// 定义People接口类型对象
let p: People = {
    username: 'll',
    age: 12,
    address:'SH',
    tel: '123456666677'
}
let res = p as Student; // 类型断言
let res2 = <Student>p; // 类型转换

let stu = new Student('lily', 23, 'BJ')
stu as People;// 类型断言

5. 如果A是类,B是type定义的对象数据类型,且A类实现了B

如果A是类,B是type定义的对象数据类型,且A类实现了B type定义的数据类型,则A的对象变量可以断言成B type定义的对象数据类型,B type对象数据类型也可以断言成A类型,即:可以相互断言。与场景3规则相同

type People = {
    username: string;
    age: number;
    address: string;
    tel: string;
}

class Student implements People {
    public username!: string;
    public age!: number;
    public address!: string;
    public tel!: string;
    constructor(username: string, age: number, address: string) {
       this.username = username;
       this.age = age;
       this.address = address;
    }
    public getAddress() {}
}

// 定义People类型对象
let p: People = {
    username: 'll',
    age: 12,
    address:'SH',
    tel: '123456666677'
}
let res = p as Student; // 类型断言
let res2 = <Student>p; // 类型转换

let stu = new Student('lily', 23, 'BJ')
stu as People;// 类型断言

6. 如果A是类,B是type定义的对象数据类型,但A类没有实现B

断言关系与场景2相同。

type People = {
    username: string;
    age: number;
    address: string;
    tel: string;
}

class Student{
    public username!: string;
    public age!: number;
    public address!: string;
    public tel!: string;
    constructor(username: string, age: number, address: string) {
       this.username = username;
       this.age = age;
       this.address = address;
    }
    public getAddress() {}
}

// 定义People类型对象
let p: People = {
    username: 'll',
    age: 12,
    address:'SH',
    tel: '123456666677'
}
let res = p as Student; // 类型断言
let res2 = <Student>p; // 类型转换

let stu = new Student('lily', 23, 'BJ')
stu as People;// 类型断言

7. 如果A是一个函数上参数变量的联合类型

例如 string | number,那么在函数内部可以断言成string或number类型

function fn(one: string | number) {
    one as string;
    one as number + 3;
}

8. 多个类组成的联合类型的断言方式

例如:let vechile: Car | Bus | Trunck,vechile可以断言成其中任意一种数据类型。

9. 任何数据类型都可以转换成any或unknown类型,any和unknown类型也可以转成任何其他数据类型

6. TS多态+类型守卫

类型守卫定义:在语句的块级作用域(if语句内或条目运算符表达式内)缩小变量的一种类型推断的行为。

类型守卫产生时机:TS条件语句中遇到下列条件关键字时,会在语句的块级作用域内缩小变量的类型,这种类型推断的行为称作类型守卫(Type Guard)。类型守卫可以帮助我们在块级作用域中获得更为需要的精确变量类型,从而减少不必要的类型断言。

  • 类型判断:typeof
  • 属性或者方法或者函数判断:in
  • 实例判断:instanceof
  • 字面量相等判断:==,===,!=,!==

多态的定义: 父类的对象变量可以接受任何一个子类的对象,从而用这个父类的对象变量来调用子类中重写的方法二输出不同的结果。

产生多态的条件:

  1. 必须存在继承关系
  2. 必须有方法重写

多态的局限性: 无法直接调用子类的独有的方法,必须结合instanceof类型守卫来解决

class People {
    public name!: string;
    constructor(name: string) {
       this.name = name;
    }
    public eat() {
         console.log('People父类的eat')
    }
}

class AmericanPeople extends People {
    constructor(name: string) {
        super(name)
     }
    public eat() { // 重写
        console.log('美国人吃饭')
   }
}

class ChinessPeople extends People {
    constructor(name: string) {
        super(name)
     }
    public eat() {// 重写
        console.log('中国人吃饭')
   }
}

class FrenchPeople extends People {
    constructor(name: string) {
        super(name)
     }
    public eat() {// 重写
        console.log('法国人吃饭')
   }
}

// 父类的对象变量people可以接受任何一个子类的对象
let people: People = new AmericanPeople('a');
people.eat() // 美国人吃饭
people = new ChinessPeople('c');
people.eat() // 中国人吃饭
people = new FrenchPeople('f');
people.eat() // 法国人吃饭

// 结合instanceof类型守卫,调用子类独有的方法
if (people instanceof ChinessPeople) {
    people.action();
}

7. TS抽象类(abstract class)

一个在任何位置都不能被实例化的类就是一个抽象类。和普通类相比,抽象类里可以定义抽象方法。

什么样的类可以被定义为抽象类? 从宏观上来说,任何一个实例化后毫无意义的类都可以被定义为抽象类。

使用抽象类的好处:

  1. 提供统一名称的抽象方法,提高代码的可维护性。抽象类通常用来充当父类,当抽象类把一个方法定义为抽象方法,那么会强制在所有子类中实现它,防止不同的子类的同功能的方法命名不同,从而降低维护项目成本。
  2. 防止实例化一个实例后毫无意义的类
abstract class People {
    public name!: string;
    // 可以有构造器
    constructor(name: string) {
       this.name = name;
    }
    // 可以有0-多个抽象方法
    public abstract eat(): void;// 抽象方法:1.没有方法体 2. abstract关键字
    
    // 可以有非抽象方法
    public sleep() {
        console.log('需要睡眠')
    }
}

class AmericanPeople extends People {
    constructor(name: string) {
        super(name)
     }
    public eat() { // 实现抽象方法
        console.log('美国人吃饭')
   }
}

class ChinessPeople extends People {
    constructor(name: string) {
        super(name)
     }
    public eat() {// 实现抽象方法
        console.log('中国人吃饭')
    }
}

class FrenchPeople extends People {
    constructor(name: string) {
        super(name)
     }
    public eat() {// 实现抽象方法
        console.log('法国人吃饭')
   }
}

// let p = new People('a') // 无法创建抽象类实例,只能子类来实例化

抽象类实现适配器:

interface MouseListennerProcess {
    mouseReleased(e: any): void; // 鼠标按钮在组件上释放时调用
    mousePressed(e: any): void;  // 鼠标按钮在组件上按下时调用
    mouseEntered(e: any): void;  // 鼠标进入到组件上时调用

    mouseClicked(e: any): void;  // 鼠标按钮在组件上单击(按下并释放)时调用
    mouseExisted(e: any): void;  // 鼠标离开组件时调用
}

// 适配器是一个抽象类
abstract class MouseListennerProcessAdapter implements MouseListennerProcess {
    mouseReleased(e: any): void {
        throw new Error("Method not implemented.");
    }
    mousePressed(e: any): void {
        throw new Error("Method not implemented.");
    }
    mouseEntered(e: any): void {
        throw new Error("Method not implemented.");
    }
    abstract mouseClicked(e: any): void 
    abstract mouseExisted(e: any): void 

}

class MyMouseListenerProcess extends MouseListennerProcessAdapter{
    mouseClicked(e: any): void {
        throw new Error("Method not implemented.");
    }
    mouseExisted(e: any): void {
        throw new Error("Method not implemented.");
    }   
}

8. 自定义守卫

自定义守卫格式:

 function 函数名(形参:参数类型【参数类型大多为any】): 形参 is A类型 {
     return true or false
 }    

返回布尔值的条件表达式赋予类型守卫的能力,只有当函数返回true时,形参被确定为A类型。 其中A类型可以是原始类型,引用类型和自定义引用类型。

// 普通判断是否是字符串类型的方式
function isString1(param: any): boolean { // 没有类型守卫功能
    return typeof param === 'string'
}

// 判断是否是字符串的自定义类型守卫
function isString2(param: any): param is string {
    return typeof param === 'string'
}

function test(str: any) {
    if (isString1(str)) {
        // 用.操作符时没有属性方法提示,说明没有类型守卫功能
        console.log(str.length) 
    }
    if (isString2(str)) {
       // 通过类型守卫,那么就可以用.操作符访问字符串类型该有的属性和方法 
       console.log(str.length) 
    }
}

9. TS泛型

1. 泛型类

泛型是一种参数化数据类型,具有以下特点的数据类型叫作泛型:

  1. 定义时不明确,使用时必须明确成某种具体数据类型的数据类型
  2. 编译期间进行数据类型安全检查的数据类型 使用泛型类的好处:
  3. 编译期对类上调用方法或属性时的泛型类型进行安全检查(类型安全检查),不符合泛型实际参数类型就编译不通过,防止不符合条件的数据增加进来。
  4. 一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,提高了代码开发效率。
   // 泛型类
   class ArrayList<T> {
    public index: number;
    public element: Array<T>;
    constructor() {
       this.element = [];
       this.index = 0;
    }

    add(ele: T) {
       this.checkIndex();
       this.element[this.index++] = ele;
    }

    checkIndex() {
        if (this.index < 0) {
            throw new Error('数组下标不能小于0')
         }
    }

    // 根据索引来查询数组中指定元素
    get(index: number) {
       return this.element[index]
    }

    show() {
        this.element.forEach(item => {
            console.log(item)
        })
    }

    remove(value: number): number;
    remove(value: T): T;
    remove(value: number | T) {
       this.element = this.element.filter((ele, index) => {
            if (typeof value === 'number') {
                return index !== value
            } else {
                return ele !== value
            }       
       })
       return value;
    }
}
let s1 = {name:'l', age:11}
let s2 = {name:'o', age:90}
let s3 = {name:'m', age:23}

// 使用时明确类型
let arrayList = new ArrayList<number>();
arrayList.add(0)
// arrayList.add('a')

let arrayList2 = new ArrayList<string>();
arrayList2.add('a')

let arrayList3 = new ArrayList<{name: string, age: number}>();
arrayList3.add(s1)

let arrayList4 = new ArrayList<typeof s1>();
arrayList4.add(s1)
arrayList4.add(s2)

type Stu = {name: string, age: number}
let arrayList5 = new ArrayList<Stu>();
arrayList5.add(s3)

arrayList.show();

2. object为什么不能替代类上的泛型?

  1. 编译期间无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查 object是一切引用类型的父类,假设只需要传递一个自定义类型People,使用时再传递其他类型不是我们想要的,但是object并不能限制,而泛型可以。

  2. object类型只能接受引用类型,原始类型无法接受,而泛型可以接受任意类型

  3. object类型数据获取属性和方法时没有自动提示

10. 理解泛型约束<T extends object, K extends keyof T>

T extends object: 泛型参数T的类型被约束(泛型约束)为object的子类,若使用时传入基本类型如string,number等会报错。

K extends keyof T: keyof T 会得到T类型的实例属性名组成的联合类型,然后K的类型只能是该联合类型的子类。

let obj = {name:'aaa', age: 23}

type Obj = typeof obj; // 等同于:type Obj = {name: string, age: number}

type KeyofObj = keyof Obj; //等同于 type KeyofObj = 'name' | 'age'

class Person {
   static nation: string;
   age: number = 0;
   name: string = 'aaa'
   sayHi(){}
}

// 类的实例属性名组成的联合类型,等同于: type P = 'age' | 'name' | 'sayHi'
type P = keyof Person; 
// 变量x的取值被只能是'age'、'name' 、'sayHi'
let x: P = 'sayHi'

class ObjectImpl<T extends object, K extends keyof T> {
   public data: T;
   public key: K;
   constructor(data: T, key: K) {
      this.data = data;
      this.key = key;
   }
   // T[K]:获取传入的对象及其传入的某个属性值的类型
   setValue(newVal: T[K]) {
      this.data[this.key] = newVal;
   }
   getValue() {
    return this.data[this.key]
   }
}
let p1 = new Person()

let o1 = new ObjectImpl(p1, 'age')
o1.setValue(13) // age属性值的类型为number,那么该函数只能传入number类型
console.log(o1.getValue()) // 13

let o2 = new ObjectImpl(p1, 'name')
o2.setValue('zoe') // name属性值的类型为string,那么该函数只能传入string类型
console.log(o2.getValue()) // zoe

11. infer关键字

infer的定义:infer表示在extends条件语句中以占位符出现的用来修饰数据类型的关键字,被修饰的数据类型等到使用时才能被推断出来。

infer占位符的关键字出现的位置:通常infer出现在以下三个位置上。

  1. infer出现在extends条件语句后的函数类型的参数类型位置上
  2. infer出现在extends条件语句后的函数类型的返回值类型上
  3. infer会出现在类型的泛型具体化类型上
class Subject {
    name: string;
    fullscore: number;
    constructor(name: string, fullscore: number) {
      this.name = name;
      this.fullscore = fullscore;
    }
 }

const s = new Subject('English', 150);

let sets = new Set<Subject>([s])
let set2 = new Array<number>()

// infer会出现在类型的泛型具体化类型上
type ElementOF<T> = T extends Set<infer E> ? E : never;

let res: ElementOF<typeof sets>; // res: Subject
let res2: ElementOF<typeof set2>; // res2: never