前端面试-TypeScript

2021/5/10 InterviewTypeScript

# 泛型的使用方式与场景

泛型的本质是参数化类型,通俗的将就是所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中,分别成为泛型类,泛型接口、泛型方法。

TypeScript中不建议使用any类型,不能保证类型安全,调试时缺乏完整的信息。

TypeScript可以使用泛型来创建可重用的组件。支持当前数据类型,同时也能支持未来的数据类型。扩展灵活。可以在编译时发现你的类型错误,从而保证了类型安全。

TypeScript中使用泛型的主要原因是使类型,类或接口充当参数。它帮助我们为不同类型的输入重用相同的代码,因为类型本身可用作参数。

// 泛型变量的使用
function identity<T>(arg: T): T {
  return arg;
}
const output1 = identity<string>('myString');
const output2 = identity('myString');
const output3: number = identity<number>(100);
const output4: number = identity(200);

// 使用集合的泛型
function loggingIdentity<T>(arg: Array<T>): Array<T> {
  return arg;
}
const res1 = loggingIdentity([1, 2, 3]);

// 泛型函数
function identity1<T>(arg: T): T {
  return arg;
}
const myIdentity1: { <T>(arg: T): T } = identity1;

// 泛型接口
interface GenericIdentityFn<T> {
  (arg: T): T;
}
function identity2<T>(arg: T): T {
  return arg;
}
const myIdentity2: GenericIdentityFn<number> = identity2;

// 泛型类
class GenericNumber<T> {
  zeroValue: T;

  add: (x: T, y: T) => T;
}
const myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };
console.info(myGenericNumber.add(2, 5));
const stringNumberic = new GenericNumber<string>();
stringNumberic.zeroValue = 'abc';
stringNumberic.add = function (x, y) { return `${x}--${y}`; };
console.info(stringNumberic.add('张三丰', '诸葛亮'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 定义Promise的返回值类型

// reslove() 方法传入的是 number 类型
const main = new Promise((resolve, reject) => {
  resolve(1);
});
// 此时main的类型为 Promise<unknown>

// 方法一:通过 Promise 的构造函数,声明返回值的泛型类型
const main1 = new Promise<number>((resolve, reject) => {
  resolve(1);
});

// 方法二:修改 reslove的类型定义
const main2 = new Promise((resolve: (value: number) => void, reject) => {
  resolve(1);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Promise返回值的类型定义,可以由两部分决定。第一个是构造时的泛型值,第二个是reslove函数value值得类型

# 联合类型与交叉类型

联合类型(Union Types):可以通过管道|将变量设置多种类型,赋值时可以根据设置的类型来赋值。只能赋值指定的类型,如果赋值其它类型就会报错。结果是这多个类型中的一个。

let union: string | number | boolean;
union = 'qwe';
union = 123;
union = false;

// 不能将类型“{}”分配给类型“string | number | boolean”
union = {};
1
2
3
4
5
6
7

交叉类型(Intersection Types):交叉类型是将多个类型合并为一个类型(取并集)。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

// 交叉类型
interface DogInterface {
  run(): void;
}
interface CatInterface {
  jump(): void;
}
const pet: DogInterface & CatInterface = {
  run() {},
  jump() {},
};

// 联合类型
class Dog implements DogInterface {
  run() {}
  eat() {}
}
class Cat implements CatInterface {
  jump() {}
  eat() {}
}
enum Master { Boy, Girl }
function getPet(master: Master) {
  // pet类型被推断为Dog和Cat的联合类型
  const pet = master === Master.Boy ? new Dog() : new Cat();
  // 如果一个对象是联合类型,在类型未确定的情况下,只能访问所有类型的共有成员(取所有类型的交集)
  pet.eat();
  // pet.run(); // 这个方法是不能使用的
  return pet;
}

// 对于基本类型
type a = string | number // type a = string | number
// 基本类型是不会存在交叉的。比如 number 和 string 是不可能有交叉点的,一个类型不可能既是字符串又是数字。
type b = string & number // type b = nerver

// 需要注意的是,访问联合类型的属性时,只能访问此联合类型的所有类型里共有的属性:
let a: string | number;
a.length; // 类型“string | number”上不存在属性“length”。类型“number”上不存在属性“length”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

总结:交叉类型是多个类型合并为一个类型,可以访问所有类型的属性;联合类型是多个类型中的某一个,只能访问所有类型的共有属性。联合类型取的是交集,交叉类型取的是并集。这听上去跟名字有些冲突,然而它们在基本类型又不是这样表现的

# TypeScript中的内置类型

链接:TypeScript中的内置类型

# 如何定义一个对象类型

interface Person {
  name: string,
  age?: number,
  // 任意属性必须是其他属性的公共父类,即此处不能单独为string或number
  [propName: string]: string | number
}

const person: Person = {
  name: '123',
  age: 123,
  sex: 'male',
};
1
2
3
4
5
6
7
8
9
10
11
12

# type与interface的区别

An interface can be named in an extends or implements clause, but a type alias for an object type literal cannot. An interface can have multiple merged declarations, but a type alias for an object type literal cannot.

  • interface可以命名在extends或者implements语句中,但是对象类型文本的类型别名(type)不能。
  • 一个接口可以合并多个声明,但对象类型文本的类型别名不能。

相同点:

  • 都可以描述一个对象或函数

    // interface
    interface User {
      name: string;
      age: number;
    }
    interface SetUser {
      (name: string, age: number): void;
    }
    
    // type
    type User = {
      name: string;
      age: number;
    };
    type SetUser = (name: string, age: number) => void;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 都允许扩展

    interfacetype都可以拓展,并且两者并不是相互独立的,也就是说interface可以extends typetype也可以extends interface。虽然效果差不多,但是两者语法不同。只要是interface扩展属性,无论是interface还是type,都使用关键字extends;type的扩展属性无论是type还是interface都是使用关键字&

    interface extends interface

    interface Name {
      name: string;
    }
    interface User extends Name {
      age: number;
    }
    
    1
    2
    3
    4
    5
    6

    type extends type

    type Name = {
      name: string;
    }
    type User = Name & { age: number };
    
    1
    2
    3
    4

    interface extends type

    type Name = {
      name: string;
    }
    interface User extends Name {
      age: number;
    }
    
    1
    2
    3
    4
    5
    6

    type extends interface

    interface Name {
      name: string;
    }
    type User = Name & {
      age: number;
    }
    
    1
    2
    3
    4
    5
    6

不同点:

  • interface可以合并,但是type不行。

    interface User {
      name: string;
      age: number;
    }
    interface User {
      sex: string;
    }
    // eslint会报重复定义
    // 等价于
    // interface User {
    //   name: string;
    //   age: number;
    //   sex: string;
    // }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • type语句中还可以使用typeof获取实例的类型进行赋值。

    // 当你想获取一个变量的类型时,使用 typeof
    const div = document.createElement('div');
    type B = typeof div
    // type B = HTMLDivElement
    
    1
    2
    3
    4
  • interface只能表示objectclassfunction类型,类型别名可以用于其它类型(联合类型、元组类型、基本类型(原始值))。

  • type支持能使用in关键字生成映射类型,interface不支持。

    语法与索引签名的语法类型,内部使用了for...in。具有三个部分:

    • 类型变量K,它会依次绑定到每个属性。
    • 字符串字面量联合的Keys,它包含了要迭代的属性名的集合。
    • 属性的结果类型。
    type Keys = 'firstname' | 'surname';
    
    type DudeType = {
      [key in Keys]: string; //映射每个参数
    };
    
    const test: DudeType = {
      firstname: 'zhang',
      surname: 'san'
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • inerface支持同时声明,默认导出,而type必须先声明后导出。

    export default interface Config {
      name: string;
    }
    
    // 同一个js模块只能存在一个默认导出哦
    type Config2 = { name: string }
    export default Config2
    
    1
    2
    3
    4
    5
    6
    7

# 函数重载

JavaScript本身是个动态语言。JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。所以需要函数重载。

函数重载具有两个特征:名称相同,参数不同(参数类型、个数不同。)所以,函数重载的解释应该是具备不同参数的同名函数。注意:函数重载是多态的一种体现。

函数重载的声明和实现:TypeScript中,函数重载主要包括两部分:函数声明,和函数实现。函数声明主要是TSC解析的一种声明体现,实际编译中,并不会编译成具体代码。我们可以通过TypeScriptplayground来查看。所以它不是真的实现像传统静态类型语言那样的重载。

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: { suit: string, card: number }[]): number;
function pickCard(x: number): { suit: string, card: number };
function pickCard(x: any): any {
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  } else if (typeof x == 'number') {
    let pickedSuit = Math.floor(x / 13);
    return {
      suit: suits[pickedSuit],
      card: x % 13,
    }
  }
}

let myDeck = [
  {
    suit: "diamands",
    card: 2,
  },
  {
    suit: 'spades',
    card: 10,
  },
  {
    suit: 'hearts',
    card: 4
  }
]

let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);

console.log('card: ' + pickedCard1.card + ' of ' + pickedCard1.suit);
console.log('card: ' + pickedCard2.card + ' of ' + pickedCard2.suit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

pickCard方法根据传入参数的不同会返回两种不同的类型。如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。如果用户想抓牌,我们告诉他抓到了什么牌。但是这怎么在类型系统里表示呢。方法是为同一个函数提供多个函数类型定义来进行函数重载。编译器会根据这个列表去处理函数的调用。

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。它查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x: any): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。以其它参数调用pickCard会产生错误(没有与此调用匹配的重载)。因此定义完重载签名之后,一定要有具体实现

# TypeScript的主要特点

  • 跨平台:TypeScript编译器可以安装在任何操作系统上,包括Windows、macOS和Linux。
  • ES6特性:TypeScript包含计划中的ECMAScript2015(ES6)的大部分特性,例如箭头函数。
  • 面向对象的语言:TypeScript提供所有标准的OOP(面向对象程序设计(Object Oriented Programming))功能,如类、接口和模块。
  • 静态类型检查:TypeScript使用静态类型并帮助在编译时进行类型检查。因此,你可以在编写代码时发现编译时错误,而无需运行脚本。
  • 可选的静态类型:如果你习惯了JavaScript的动态类型,TypeScript还允许可选的静态类型。
  • DOM 操作:您可以使用TypeScript来操作DOM以添加或删除客户端网页元素。

# TypeScript有什么好处

  • TypeScript更具表现力,这意味着它的语法混乱更少。
  • 由于高级调试器专注于在编译时之前捕获逻辑错误,因此调试很容易。
  • 静态类型使TypeScript比JavaScript的动态类型更易于阅读和结构化。
  • 由于通用的转译,它可以跨平台使用,在客户端和服务器端项目中。

# TypeScript中的模块

TypeScript中的模块是相关变量、函数、类和接口的集合。

“内部模块”现在称做“命名空间”。“外部模块”现在则简称为“模块”,这是为了与ECMAScript2015里的术语保持一致(也就是说module X {}相当于现在推荐的写法namespace X {})。

若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。

# TypeScript类的Getter,Setter,static

class User {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  get nameXXX() {
    return this.name;
  }

  set nameXXX(name: string) {
    this.name = name;
  }
  // 无需new直接用,User.getName()
  static getName(): string {
    return this.name
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 三斜线指令

三斜线指令是单行注释,包含用作编译器指令的XML标记。每个指令都表示在编译过程中要加载的内容。三斜杠指令仅在其文件的顶部工作,并且将被视为文件中其他任何地方的普通注释。

引用不存在的文件会报错。一个文件用三斜线指令引用自己会报错。

/// <reference path="..." /> 是最常见的指令,定义文件之间的依赖关系。
/// <reference types="..." />类似于path但定义了包的依赖项。
/// <reference lib="..." />允许您显式包含内置lib文件。
1
2
3

# 实现过滤可选属性

interface Person {
  name: string;
  age: number;
  sex?: string;
  job?: string;
}

type RequiredParams = GetReqiured<Person>
// { name: string; age: number; }
1
2
3
4
5
6
7
8
9

请实现GetReqiured

参考链接:常用工具类型-过滤可选属性

# const和readonly的区别,枚举和常量枚举的区别

readonly只读修饰符,可以声明更加严谨的可读属性,通常在interface、Class、type以及array和tuple类型中使用它,也可以用来定义一个函数的参数。

  • const用于变量,readonly用于属性。

  • const在运行时检查,readonly在编译时检查。

  • const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值;readonly修饰的属性能确保自身不能修改属性,但是当你把这个属性交给其它并没有这种保证的使用者(允许出于类型兼容性的原因),他们能改变。

    const foo: { readonly bar: number; } = { bar: 123 };
    function iMutateFoo(foo: { bar: number }) {
      foo.bar = 456;
    }
    iMutateFoo(foo);
    console.log(foo.bar); // 456
    
    1
    2
    3
    4
    5
    6
  • const保证的不是变量的值不得改动,而是变量指向的那个内存地址不得改动,例如使用const变量保存的数组,可以使用pushpop等方法。但是如果使用ReadonlyArray声明的数组不能使用pushpop等方法。

枚举和常量枚举

// 枚举
enum Color {
  Red,
  Green,
  Blue
}
// 常量枚举
const enum Color {
  Red,
  Green,
  Blue
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 枚举会被编译时会编译成一个对象,可以被当作对象使用。
  • const枚举会在typescript编译期间被删除,const枚举成员在使用的地方会被内联进来,避免额外的性能开销。

# undefined,null,unknown,any,void,never的区别

  • any:任意类型,允许你在编译时可选择地包含或移除类型检查,可以在它上面调用任意的方法。

  • unknown:指的是不可预先定义的类型,在很多场景下,它可以替代any的功能同时保留静态检查的能力。

    anyunknown都是顶级类型,任何类型都可以赋值给它们,但是unknown更加严格,不像any那样不做类型检查,反而unknown因为未知性质,不允许访问任何属性和方法(即使它有这个属性和方法),不允许赋值给其他有明确类型的变量

  • 默认情况下nullundefined是所有类型的子类型。就是说你可以把nullundefined赋值给number类型的变量。然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。这能避免很多常见的问题。也许在某处你想传入一个stringnullundefined,你可以使用联合类型string | null | undefined

  • void:表示没有任何类型。声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

  • never:类型表示的是那些永不存在的值的类型。never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。即使any也不可以赋值给never。(如果"strictNullChecks": truenever也不能赋值给never)。never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型(例如抛出异常,死循环),变量也可以直接申明为never类型,让它永不存在值,其实就是意思就是永远不能给它赋值,否则就会报错,这样就可以形成一种保护机制。

最近更新: 2025年03月02日 21:14:11