2022面试准备:17个TypeScript知识点与TypeScript面试29问!

1,693 阅读33分钟

我正在参加「掘金·启航计划」

TypeScript有什么知识点?

1 基础类型

Typescript中的数据类型为:布尔值、数字、字符串、数组、元组、枚举、空值、任意值、类型断言。

  • null和undefined:默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给其他类型。
  • 元组:一个已知元素数量和类型的数组,各元素的类型不必相同。
  • 类型断言:手动指定一个值的类型,告诉编译器我知道自己在干嘛。注意不能直接从string指定为number,类型断言在原类型为any或unknown才能手动指定类型。
let isShow:boolean = false;// 布尔值
let num:number=1;// 数字
let name:string="Tom";// 字符串
let arr:number[]=[1,2,3];// 数组定义1
let arr:Array<any> = [1,2,3];// 数组定义2:数组泛型
let x:[string,number]=["Tom",21];// 元组,包含两个元素的数组。
enum Color{red,blue,green};// 枚举
let x:Color=Color.red;// 0 
let x:any;// 任意值,可以赋值任何类型值 
const str:any = "123";
const num:number=(<string>str).length;// 123 ,string类型不能直接手动转为number,需要先转为任意类型再转
const num:number=(str as string).length;

2 TypeScript 迭代器(Iterator)与TypeScript 生成器(Generator)

2.1 TypeScript 迭代器(Iterator)

迭代器的作用:

  • 为各种数据结构(Array、Map、Set、String等),提供一个统一的、简便的访问接口
  • 使得数据结构得成员能够按某种次序排列
  • 创建一种新得遍历命令for...of循环。
    interface IteratorInterface {
      next: () => {
        value: any
        done: boolean
      }
    }

    function createIterator(array: any[]): IteratorInterface {
      let index = 0
      let len = array.length

      return {
        next: function () {
          return index < len ? { value: array[index++], done: false } : { value: undefined, done: true }
        }
      }
    }

    var iterator = createIterator([1, 2, 3])

    console.log(iterator.next()) // { value: 1, done: false }
    console.log(iterator.next()) // { value: 2, done: false }
    console.log(iterator.next()) // { value: 3, done: false }
    console.log(iterator.next()) // { value: undefined, done: true }

2.2 TypeScript 生成器(Generator)

通过function* 创建一个生成器函数,再调用一个生成器函数后,并不会立即执行函数中的代码,而是返回一个迭代器对象。

function* gen() { 
  console.log('开始执行')
  let res1 = yield 1
  console.log('中断后继续执行')
  console.log(res1)
  
  let res2 = yield 2
  console.log(res2)
  
  console.log('执行结束')
  return 3
}

let iterator = gen()
console.log(iterator.next('first'))
console.log(iterator.next('second'))
console.log(iterator.next('third'))

3 接口

在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作规范。在程序设计时,接口起到一种限制和规范的作用

3.1 属性类接口

interface Bag{
  pen?:string;// 可选属性
  weight:number;
  readonly x:number;// 只读属性,只有初始化的时候可以修改其值
}

3.2 函数类接口

interface ComFunc{
  (source:string,subString:string):boolean;
}

let fun:ComFunc;
fun = function(source:string,subString:string){
  // 如果这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与ComFunc接口中的定义不匹配。
}

3.3 可索引接口

可索引类型具有一个索引签名,它描述了对象索引的类型,还由相应的索引返回值类型

interface ArrayType{
  [index:number]:string;// [索引类型]:返回值类型
}

let myArray:ArrayType;
myArray = ["Bob","Tom"];
let myStr:string = myArray[0];

可索引接口支持两种索引签名:字符串和数字。可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。这是因为当使用number来索引时,JavaScript会将它转换成string然后去索引对象。

class Animal{
    name:string='';
}

class Dog extends Animal {
    breed:string='';
}

interface Type{
    [x:string]:Animal;
    [x:number]:Dog;
}

还有字符串索引签名能够很好的藐视dictionary,并且它们也会确保所有属性与其返回值类型相匹配。

interface Type{
  [index:string]:number;
  length:number;
  name:string;// 错误,name的类型不是索引类型的子类型,
}

3.4 类类型接口

与C#或Java里接口的基本作用一样,ts也能够用它来明确的强制一个类去符合某种契约。也就是通过类去实现(重写)接口的功能(变量、方法等)

interface RootNode{
    firstName:string;
}

class children implements RootNode{
    firstName:string='';
    constructor(num1:number,num2:number){

    }
}

3.5 接口扩展

和类一样,接口也可以相互扩展,从一个接口复制成员到另一接口,灵活地将接口分割到可复用地模块里。一个接口可以继承多个接口,创建出多个接口地合成接口。

interface Apple{
  appleArice:number;
}

interface Pork{
  porkArice:number;
}

interface Food extends Apple,Pork{
  
}

3.6 接口继承类

当接口继承了一个类类型时,它会继承类地成员但不包括其实现。这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现。

class Control {
    private state: any;
}
interface SelectableControl extends Control {
    select(): void;
}
class Button extends Control {
    select() { }
}

4 类

从ECMAScript2015(es6),Javascript程序将支持使用这种基于类的面向对象方法。在ts里,我们允许开发者现在就使用这些特性,并且编译后的js可以在所有主流浏览器和平台上运行,而不需要等到下一个Javascript版本。

类静态化:变量声明带static,通过类.变量名调用

类实例化:通过实例去修改或调用类的方法

4.1 类的继承

子类继承父类(基类),子类可以访问父类的属性和方法。注意地,包含构造函数(constructor)的派生类必须调用super(),它会执行基类的构造函数

class Parent{
   father:string='张三';
   mother:string='李四';
   constructor(f:string,m:string){
    this.father = f;
    this.mother = m; 
   }

   pMethod(){
    console.log(this.father+this.mother);
   }
}

class child extends Parent{
    constructor(f:string,m:string){
        super(f,m);
    }
    cMathod(){
        super.pMethod();
    }
}

let obj = new child("王五","周六");
obj.cMathod();

4.2 修饰符

  • public(在ts中,每个成员默认为public的)

  • private(它不能在声明它的类的外部访问)

  • protected(与private类似,但它可以在派生类中可以访问)

  • readonly修饰符(必须在声明时或构造函数里被初始化)

class Animal{
   public hand:string='手';
   private foot:string='脚';
   protected ear:string='耳朵';
   readonly eye:string='眼睛';
   constructor(e:string){
     this.ear = e;
   }
}

class Dog extends Animal{
    constructor(f:string){
        super(f);
    }

}

let obj = new Dog("两个耳朵",);
console.log(obj.eye);// 类外访问protected变量
console.log(obj.hand);// 类外部访问public变量
// 其他变量都不能在类外访问
  • 参数属性:方便我们在一个地方定义并初始化一个成员。
class Animal{
   constructor(private name:string){
   }
   method(){
    console.log(this.name);
   }
}
let obj = new Animal("参数属性");
obj.method();
  • 存取器:ts支持get/set来截取对对象成员的访问,它能帮助你有效的控制对象成员的访问。
class Animal{
   private _fullName:string='';
   
   get fullName(){
    return this._fullName;
   }

   set fullName(newName:string){
     this._fullName = newName;
   }
}
let obj = new Animal();
obj.fullName = "孙悟空";
console.log(obj.fullName);
  • 静态属性:通过类来调用。
class Animal{
  static fullName="孙悟空";
}
  • 抽象类:拱其他类继承的基类,它们一般不会直接被实例化,但是抽象类中的抽象方法、变量必须被派生类实现。
abstract class Animal{
   abstract fullName:string;
   
   abstract method():void;
}
class dog extends Animal{
    fullName:string='';
    method(){
        console.log("抽象类中方法重写!");
    }
}
let a = new dog();

5 泛型

使用类型变量表示类型而不是值(也就是下面的T),T可以帮助我们捕获用户传入的类型。

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

4.1 泛型类型

  • 使用不同泛型参数名
    function fn<T>(arg:T):T{
       return arg;
    }
    let myFun:<T>(arg:T)=>T=fn;
    let myFun:<U>(arg:U)=>U=fn;// 可以使用不同的泛型参数名
    
  • 可以使用带有调用签名的对象字面量来定义泛型函数
    function fn<T>(arg:T):T{
       return arg;
    }
    let myFun:{<T>(arg:T):T}=fn;
    

4.2 泛型接口

interface fnType{
<T>(arg:T):T;// 接口定义方法类型1
}

function fn<T>(arg:T):T{
   return arg;
}
let myFun:fnType=fn;// 声明方法类型1
interface fnType<T>{
  (arg:T):T;// 接口定义方法类型2
}

function fn<T>(arg:T):T{
   return arg;
}
let myFun:fnType<T>=fn;// 声明方法类型2

4.3 泛型类

class Animal<T>{
  ear:T;
  skill:(x:T,y:T)=>T
}
let dog = new Animal<number>();

4.4 泛型约束

操作某类型的一组值,并且我们这组值有什么样的属性。所以就需要对传入的类型进行限制,下面定义一个接口来描述约束条件,此时需要传入符合约束类型的值,必须包含必须的属性

interface LenType{
  length:number;
}
function LenType<T extends LenType>(arg:T):T{
  console.log(arg.length);
  return arg;
}

6 函数

6.1 函数类型

  • 普通函数

    const add = function(x: number, y: number): string {
      return (x + y).toString()
    }
    
  • 箭头函数

    const add: (x: number, y: number) => string = function(x: number, y: number): string {
      return (x + y).toString()
    }
    

6.2 函数参数

  • 可选参数:可填可不填的参数,可选参数必须跟在必须参数后面

    // 必须参数firstName
    // 可选参数lastName
    const fullName = (firstName: string, lastName?: string): string => `${firstName}${lastName}`
    
  • 默认参数:带默认值的参数。

    const token = (expired = 60*60, secret: string): void  => {}
    
  • 剩余参数:函数的参数个数是不确定的,通过rest参数(...变量名)来获取函数的剩余参数,这样就不需要用arguments对象,rest参数只能是最后一个参数

    function assert(ok: boolean, ...args: string[]): void {
      if (!ok) {
        throw new Error(args.join(' '));
      }
    }
    
  • this参数:

    默认情况下,tsconfig.json 中,编译选项 compilerOptions 的属性 noImplicitThis 为 false,我们在一个对象中使用的 this 时,它的类型是 any 类型。为true时,TypeScript编译器就会帮你进行正确的类型推断。

    interface Triangle {
      a: number;
      b: number;
      c: number;
      area(this: Triangle): () => number;
    }
    let a = 12;
    let b = 17;
    let c = 20;
    let triangle: Triangle = {
      a: 10,
      b: 15,
      c: 20,
      area: function (this: Triangle) {
        console.log(this); // {  "a": 10,"b": 15,"c": 20}
        return () => {
          const p = (this.a + this.b + this.c) / 2;// 22.5
          return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
        }
      }
    }
    
    const myArea = triangle.area()
    console.log(myArea())
    

6.3 函数重载

函数重载是指函数根据传入不同的参数,返回不同类型的数据

function reverse(target: string | number) {
  if (typeof target === 'string') {
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') {
    return +[...target.toString()].reverse().join('')
  }
}

console.log(reverse('imooc'))   // coomi
console.log(reverse(23874800))  // 847832

6.4 使用函数注意事项

  • 函数没有使用return语句,则它默认返回undefined。
  • 调用函数时,传递给函数的值为函数实参(值传递),对应位置的函数参数被称为形参(也就是全局没有声明,根据函数需要自定义的值)。
  • 函数执行时,this关键字并不会指向正在运行的函数本身,而是指向调用函数的对象。
  • arguments 对象是所有(非箭头)函数中都可用的 局部变量。你可以使用 arguments 对象在函数中引用函数的参数。

7 类型推论与兼容

7.1 类型推论

  • 基础类型推断:类型是在哪里如何被推断的。

    let x = 3;
    // 变量类型被推断为数字
    // 发生在
    // 初始化变量和成员
    // 默认参数值和决定函数返回值时
    
  • 推断合适通用类型:计算通用类型算法会考虑候选类型,并给出一个兼容所有候选类型的类型

    let x = [0, 1, null];
    // 有两个候选类型选择number和null
    
  • 上下文类型推断:前面两种都是根据从右向左流动进行类型推断,上下文类型推断则是从左向右的类型推断。

    class Animal {
      public species: string | undefined
      public weight: number | undefined
    }
    
    const simba: Animal = {
      species: 'lion',
      speak: true  // Error, 'speak' does not exist in type 'Animal'
    }
    

7.2 类型兼容

ts里的类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。

  • 变量属性兼容:x要兼容y,那么y至少具有与x相同的属性

    interface Named {
        name: string;
    }
    
    let x: Named;
    let y = { name: 'Alice', location: 'Seattle' };
    x = y;
    
  • 函数参数兼容

    let x = (a: number) => 0;
    let y = (b: number, s: string) => 0;
    
    y = x; // OK
    x = y; // Error
    
  • 枚举兼容:枚举类型与数字类型兼容,不同枚举类型之间是不兼容的。

  • 类兼容

    🍑 比较两个类类型对象时,只有实例的成员会被比较。静态成员和构造函数不在比较的范围内。

    class Animal {
        feet: number;
        constructor(name: string, numFeet: number) { }
    }
    
    class Size {
        feet: number;
        constructor(numFeet: number) { }
    }
    
    let a: Animal;
    let s: Size;
    
    a = s;  //OK
    s = a;  //OK
    

    🍑 类的私有成员会影响兼容性判断。当类的实例用来检查兼容时,如果它包含一个私有成员,那么目标类型必须包含来自同一个类的这个私有成员。这允许子类赋值给父类,但是不能赋值给其他有同样类型的类 。

  • 泛型兼容:ts四结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。

    🍑 x和y兼容的,因为它们的结构使用类型参数时并没有什么不同。

    interface Empty<T> {
    }
    let x: Empty<number>;
    let y: Empty<string>;
    
    x = y;  // okay, y matches structure of x
    
    interface NotEmpty<T> {
        data: T;
    }
    let x: NotEmpty<number>;
    let y: NotEmpty<string>;
    
    x = y;  // error, x and y are not compatible
    

8 高级类型

  • 联合类型:表示一个值可以说几种类型之一。用|分隔每个类型。 注意地,如果一个值是联合类型,我们只能访问此联合类型地所有类型里共有地成员

    interface Bird {
        fly();
        layEggs();
    }
    
    interface Fish {
        swim();
        layEggs();
    }
    
    function getSmallPet(): Fish | Bird {
        // ...
    }
    
    let pet = getSmallPet();
    pet.layEggs(); // okay
    pet.swim();    // errors
    
  • ts类型保护机制:一些表达式,它们会在运行时检查某个作用域里地类型。

    🍑 自定义的类型保护:et is Fish就是类型断言。 一个断言是parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名

    function isFish(pet: Fish | Bird): pet is Fish {
        return (<Fish>pet).swim !== undefined;
    }
    if (isFish(pet)) {
        pet.swim();
    }
    else {
        pet.fly();
    }
    

    🍑 typeof类型保护:

    function padLeft(value: string, padding: string | number) {
        if (typeof padding === "number") {
            return Array(padding + 1).join(" ") + value;
        }
        if (typeof padding === "string") {
            return padding + value;
        }
        throw new Error(`Expected string or number, got '${padding}'.`);
    }
    

    🍑 instanceof类型保护:通过其构造函数来细化其类型。 instanceof的右侧要求为一个构造函数,TypeScript将细化为:

    1) 这个函数的prototype属性,如果它的类型不为any的话。

    2) 类型中构造签名所返回的类型的联合,顺序保持一致。

    🍑 交叉类型:如Person & Serializable & Loggable,同时是PersonSerializableLoggable。 就是说这个类型的对象同时拥有这三种类型的成员。

  • 类型别名:会给一个类型起个新名字,可以作用于原始值,联合类型元组以及其它任何你需要手写的类型。(如下面string别名Name、NameResolver)

    type Name = string;
    type NameResolver = () => string;
    type NameOrResolver = Name | NameResolver;
    function getName(n: NameOrResolver): Name {
        if (typeof n === 'string') {
            return n;
        }
        else {
            return n();
        }
    }
    

    🍑 别名可以是泛型,在别名声明的右侧传入

    type Container<T> = { value: T };
    

    🍑 可以在属性里引用自己

    type Tree<T> = {
        value: T;
        left: Tree<T>;
        right: Tree<T>;
    }
    

    🍑 不能出现在声明右侧以外的地方

    type Yikes = Array<Yikes>; // 错误
    
  • 接口VS类型别名:类型别名不能被extends和implements也不能去extends和implements其他类型。因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

9 命名空间

  • 命名空间:一种语法糖,通过namespace关键字声明。

    // ts
    namespace Util{
      function isString(value:any){
        return typeof value==="string";
      }
    }
    
    // 生成js代码:
    var Util;
    (function(Util){
       function isString(value){
         return typeof value==='string';
       }
    })(Util||(Util={}))
    

    上面例子命名空间被转换成立即执行的函数表达式。

    🍑 命名空间特点1:命名空间内部可以使用绝多大数语言功能,如变量声明、函数声明、接口声明和命名空间声明等。

    🍑 命名空间特点2:命名空间外部使用命名空间内部某个声明需要使用导出声明语句明确地导出该声明。

    namespace Util{
       export function isString(value:any){
        return typeof value==="string";
      }
    }
    
    Util.isString("");
    

10 模块

es2015开始,js引入模块,ts也沿用这个概念。

  • 模块:在其自身的作用域里执行,而不是在全局作用域里;定义在一个模块里的变量,函数,类等等在模块外部都是不可见的,除非export出;要想使用其他模块导出的变量,需要导入import。一个模块可以声明对其他模块的依赖,且模块之间只能通过模块的公共API进行交互。

    🍑 ComomonJS一个主要用于服务端js程序的模块系统。主要应用场景是Nodejs程序中,在Nodejs中,每一个文件都会被视为一个模块。浏览器环境中的js引擎不支持CommonJs模块,因此无法直接运行使用CommonJS模块的代码;CommonJS模块采用同步的方式加载模块文件,这种加载方式不适用于浏览器环境,因为在浏览器中同步地加载模块文件会阻塞用户操作,从而带来不好地用户体验。

    // utils.js
    exports.add = function(x,y){
      return x+y;
    }
    
    // index.js
    const utils = require('./utils);
    ...
    

    🍑 AMD(Asynchronous Module Definition):异步模块定义,AMD模块允许一个文件同时定义多个模块

    // utils.js
    define(['require','exports'],function(require,exports){
         function add (x,y){
           return x+y;
         }
    })
    
    // index.js
    define(['require','exports','./utils'],function(require,exports,utils){
        const total = utils.add(1,2);
    })
    

    🍑 UMD(Universal Module Definition,通用模块定义):由于CommonJS模块不能在浏览器中使用,AMD模块也不能在Nodejs中使用,AMD就是为了解决这两个问题,AMD基于AMD模块地定义,并且针对CommonJS模块定义进行了适配。(可以在浏览器中使用,也可以在Nodejs中使用)。

    // utils.js
    (function(factory){
      if(typeof module === 'object' && typeof module.exports === 'object'){
        var v = factory(require,exports);
        if(v!==undefined) module.exports = v;
      }else if(typeof define === 'function'&& define.amd){
        define(['require','exports','./utils'],factory);
      }
    })(function(require,exports){
       function add(x,y){
         return x+y;
       }
       exports.add = add;
    })
    
    //index.js
    (function(factory){
      if(typeof module === 'object' && typeof module.exports === 'object'){
        var v = factory(require,exports);
        if(v!==undefined) module.exports = v;
      }else if(typeof define === 'function'&& define.amd){
        define(['require','exports','./utils'],factory);
      }
    })(function(require,exports){
       const util = require('./utils');
    })
    

    🍑 ES模块:简称ESM,是正式的语言内置模块标准,而CommonJS、AMD等都属于非官方标准。在谷歌、火狐等最新版浏览器以及Nodejs环境上已经能够支持ES模块。

    // utils.js
    exports.add = function(x,y){
      return x+y;
    }
    
    // index.js
    import {add} from "./utils";
    ...
    
  • 模块导入导出方式

    🍑 重命名模块导入导出

    // 导出
    const a = 0;
    export { a as x };
    
    // 将导出mod模块内的oldName声明重命名
    export { oldName as newName } from "mod";
    
    /// 导入
    import { oldName as newName } from "mod";
    

    🍑 针对类型的模块导入导出:在导入导出添加type关键字。import type和export type。import type 是用来协助进行类型检查和声明的,在运行时是完全不存在的,也就是说在编译生成js代码时,编译器一定会删除import type和export type语句,因能够完全确定它们只与类型相关。 🍑 动态模块导入

    import('./utils').then((util)=>{
      util.add(1,2);
    })
    
  • 模块解析

    🍑 Classic策略:将模块名视为一个文件进行解析

    1)解析相对模块导入(只会查找指定目录): 第一阶段,将导入的模块视为文件,并在指定目录中查找TypeScript文件;第二阶段,将导入的模块视为文件,并在指定目录中查找JavaScript文件

    // 相对模块导入
    import * as B from '../b'
    

    2)解析非相对模块(会向上解析遍历整个目录树)导入:第一阶段,遍历目录树并查找TypeScript文件;第二阶段,遍历目录树并在每一级目录下查找是否在“node_modules/@types”文件夹中安装了要导入的声明文件。第三阶段,遍历目录树并查找JavaScript。

    // 非相对模块导入
    import * as B from 'b'
    

    在查找模块文件的过程中,一旦找到匹配的文件,就会停止搜索。

    🍑 Node策略

    1)解析相对模块导入:第一阶段,将导入的模块名视为文件,并在指定目录中查找TypeScript文件。第二阶段,将导入的模块名视为目录,并在该目录中查找“package.json”文件,然后解析“package.json”文件中的typings属性和types属性。第三阶段,将导入的模块名视为文件,并在指定目录中查找JavaScript文件;第四阶段,将导入的模块名视为目录,并在该目录中查找“package.json”文件,然后解析“package.json”文件中的main属性

    2)解析非相对模块导入:第一阶段,将导入的模块名视为文件,并在当前目录下的“node_modules”文件夹中查找Type-Script文件;第二阶段,将导入的模块名视为目录,并在当前目录下的“node_modules”文件夹中查找给定目录下的“package.json”文件,然后解析“package.json”文件中的typings属性和types属性;第三阶段,将导入的模块名视为安装的声明文件,并在当前目录下的“node_modules/@types”文件夹中查找安装的声明文件;第四阶段,重复第1~3步的查找过程,从当前目录开始向上遍历至系统根目录。;第五阶段,将导入的模块名视为文件,并在当前目录下的“node_modules”文件夹中查找JavaScript文件。;第六阶段,)将导入的模块名视为目录,并在当前目录下的“node_modules”文件夹中查找给定目录下的“package.json”文件,然后解析“package.json”文件中的main属性。;第七阶段,重复第5~6步的查找过程,从当前目录开始向上遍历至系统根目录。;

11 .d.ts文件(类型声明文件)

  • .d.ts文件:只提供类型声明,不提供任何值。因为在编译ts程序的过程中,.d.ts文件不会生成对应的.js文件。

  • 声明方式

    🍑 类型声明

    // index.d.ts
    declare var a:boolean;
    declare let b:boolean;
    declare const c:boolean;
    

    🍑 模块声明:

    declare module 'io'{
      export function readFile(filename:string):string;
    }
    
    import {readFile} from 'io';
    const content:string = readFile('hello.ts')
    
  • 声明使用:.d.ts声明文件主要有这几种来源:TypeScript语言内置的声明文件、安装的第三方声明文件、自定义的声明文件 1)TypeScript语言内置的声明文件:TypeScript语言内置的声明文件统一使用“lib.[description].d.ts”(lib.es5.d.ts)命名方式。 2)安装的第三方声明文件: 3)自定义的声明文件

12 声明合并

声明合并:当编译器发现同一声明空间内存在同名的声明时,会尝试将所有同名的声明合并为一个声明。

12.1 接口声明合并

interface A{
  a:string;
}

interface A{
  b:number;
}

interface MergedA{
  a:string;
  b:number;
}

注意:如果同名接口里有同名属性类型不一致会报错,只有同名同属性同类型或同名不同属性编译才能通过。

12.2 枚举声明合并

enum E{
  A,
}

enum E{
  B=1,
}
let e:E;
e = E.A;
e = E.B;

注意地,多个同名地枚举声明必须同时为const枚举或非const枚举,不允许混合使用

12.3 类声明合并

接口不支持合并同名的类声明,但是外部类声明可以与接口进行合并,合并后的类型为类类型。

declare class C{
  x:string;
}

interface C{
  y:number
}
let c:C = new C();
c.x;
c.y;

12.4 命名空间声明合并

  • 命名空间与命名空间:

    namespace Animals{
      export class Bird{}
    }
    namespace Animals{
      export class Dog{}
    }
    namespace Merge{
      export class Bird{}
      export class Dog{}
    }
    
  • 命名空间与函数的合并:

    function f(){
      return f.version;
    }
    
    namespace f{
      export const version = '1.0';
    }
    f();//'1.0'
    f.version;//'1.0'
    
  • 命名空间与类的合并:

    class A{
      foo:string = A.bar;
    }
    namespace A{
      export let bar = 'A';
      export function create(){
        return new A();
      }
    }
    
    const a:A = A.create();
    a.foo;// 'A'
    A.bar;// 'A'
    
  • 命名空间与枚举合并

    enum E{
     A,B,C
    }
    

13 三斜线指令

  • 三斜线指令:通过它来定义文件间的依赖,编译器就能够识别出“b.ts”依赖于“a.ts”,但是一定要保证编译器文件加载顺序a比b早(在tsconfig.ts中设置)。比如以下文件

    // a.ts
    namespace Util{
       export function isString(value:any){
        return typeof value==="string";
      }
    }
    
    //b.ts
    /// <reference path="a.ts"> 声明ts源文件之间的依赖关系
    /// <reference types=""> 对安装在"node_modules/@types"目录下的某个声明文件的依赖
    /// <reference lib=""> 用于定义对语言内置的某个声明文件的依赖
    namespace App{
     const a = isString('foo')
    }
    

14 装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为

  • 启用装饰器特性

    🍑 命令行

    tsc --target ES5 --experimentalDecorators
    

    🍑 tsconfig.json

    {
        "compilerOptions": {
            "target": "ES5",
            "experimentalDecorators": true
        }
    }
    
  • 装饰器的写法

    🍑 普通装饰器(无法传参)

    function logClass(params:any){
      // params 就是当前类
      params.prototype.apiUrl =  '动态扩展的属性';
      params.prototype.run = function(){
        console.log('我是一个run方法!');
      }
    }
    
    @logClass
    class classClient{
       construct(){}
    }
    
    let a:any = new classClient();
    console.log(a.apiUrl);
    

    🍑 装饰器工厂(可传参)

    function logClass(params: string) {
        return function (target: any) {
            console.log(target);
            console.log(params);
            target.prototype.apiUrl = params;
        }
    }
    @logClass('http://www.itying.com/api')
    class classClient {
        constructor() {}
    }
    var a: any = new classClient();
    console.log(a.apiUrl);
    
  • 装饰器种类:

    🍑 类装饰器

    🍑 属性装饰器

    🍑 方法装饰器

    🍑 参数装饰器

    🍑 访问器装饰器

14.1 类装饰器

类装饰器表达式在运行时当作函数被调用,类的构造函数作为其唯一的参数。

//类装饰器
function logClass(params: string) {
    return function (target: any) { 
       // console.log(target);//class类
       // console.log(params);//xxxx
    }
}
@logClass('xxxx')
class HttpClient {
    constructor() {}
}
var http = new HttpClient();

14.2 属性装饰器

作用于类属性的装饰器表达式会在运行时当作函数被调用,传入下列3个参数target、name、descriptor:

  • target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • name:成员的名字。
  • descriptor:成员的属性描述符。

如果你熟悉 Object.defineProperty,你会立刻发现这正是 Object.defineProperty 的三个参数。

function readonly(value: boolean) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    descriptor.writable = value
  }
}

class Employee {
  @readonly(false)
  salary() {
    console.log('这是个秘密')
  }
}

const e = new Employee()
// e.salary = () => { // Error,不可写
//   console.log('change')
// }
e.salary()

14.3 方法装饰器

function enumerable(bool: boolean): any {
  return function(
    target: any,
    propertyName: string,
    descriptor: PropertyDescriptor
  ) {
    return {
      value: function() {
        return "not age";
      },
      enumerable: bool
    };
  };
}
class Info {
  constructor(private age: number) {}
  @enumerable(false)
  getAge() {
    return this.age;
  }
}
const info = new Info(25);
console.log(info.getAge()); // "not age"

14.4 参数装饰器

参数装饰器表达式会在运行时当作函数被调用,以使用参数装饰器为类的原型上附加一些元数据,传入下列3个参数target、name、index:

  • target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • name:成员的名字
  • index:参数在函数参数列表中的索引
function log(param: string) {
  console.log(param)

  return function (target: any, name: string, index: number) {
    console.log(index)
  }
}

class Employee {
  salary(@log('IT') department: string, @log('John') name: string) {
    console.log('这是个秘密')
  }
}
// "IT" 
// "John" 
// 1 
// 0 

14.5 装饰器执行顺序

function extension(params: string) {
  return function (target: any) {
    console.log('类装饰器')
  }
}

function method(params: string) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    console.log('方法装饰器')
  }
}

function attribute(params: string) {
  return function (target: any, name: string) {
    console.log('属性装饰器')
  }
}

function argument(params: string) {
  return function (target: any, name: string, index: number) {
    console.log('参数装饰器', index)
  }
}

@extension('类装饰器')
class Employee{
  @attribute('属性装饰器')
  public name!: string

  @method('方法装饰器')
  salary(@argument('参数装饰器') name: string, @argument('参数装饰器') department: string) {}
}
// 属性装饰器
// 参数装饰器 1
// 参数装饰器 0
// 方法装饰器
// 类装饰器

15 mixins(混入)

在TypeScript中,可以根据不同的功能定义多个可复用的类,它们将作为mixins。因为extends只支持继承一个父类,我们可以通过implements来连接多个mixins,并且使用原型链链接子类的方法和父类的方法。

  • 定义两个类,将它们作为mixins

    // Disposable Mixin
    class Disposable {
        isDisposed: boolean;
        dispose() {
            this.isDisposed = true;
        }
    
    }
    
    // Activatable Mixin
    class Activatable {
        isActive: boolean;
        activate() {
            this.isActive = true;
        }
        deactivate() {
            this.isActive = false;
        }
    }
    
    class SmartObject implements Disposable, Activatable {
      // 把类当成接口,仅使用Disposable和Activatable的类型而非其实现
    }
    
  • 为将要从mixin进来的属性方法创建出占位属性,这告诉编译器这些成员在远行时是可用的。

    class SmartObject implements Disposable, Activatable {
          // Disposable
        isDisposed: boolean = false;
        dispose: () => void;
        // Activatable
        isActive: boolean = false;
        activate: () => void;
        deactivate: () => void;
    }
    
  • 创建帮助函数,帮我们做混入操作

    function applyMixins(derivedCtor: any, baseCtors: any[]) {
        baseCtors.forEach(baseCtor => {
            Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
                derivedCtor.prototype[name] = baseCtor.prototype[name];
            })
        });
    }
    
    applyMixins(SmartObject, [Disposable, Activatable]);
    

16 tsconfig.json

如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。 tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。

  • 使用tsconfig.json方式 🍑 不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。 🍑 不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。
{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

17.ESLint规范TypeScript

  • 安装ESLint规范TypeScript代码所需依赖包:

    npm install -d eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
    
  • 在项目根目录下创建.eslintrc.js文件进行配置:

    module.exports = {
      root: true,
      env:{                         
        browser: true,
        node: true,
      }  
      parser:  '@typescript-eslint/parser',
      extends: [
        'plugin:@typescript-eslint/recommended'
      ],
      plugins: [
        '@typescript-eslint'
      ],
      rules: {
    
      }                      
    }
    

面试官会问什么?

1.说说你对ts的理解?与js的区别?

Typescript是一种静态类型语言,提供了类型注解,在代码编译阶段就可以检查数据类型的错误。是Javascript的类型超集,支持es6语法,支持面向对象编程的概念,如类、接口、继承、泛型等。

typescript的特性:类型批注和编译时类型检查、类型推断、类型擦除、接口、枚举、混合、泛型编程、名字空间、元组...

分类JavaScriptTypeScript
语言脚本语言面向对象编程语言
类型轻量级解释编程语言强类型面向对象编程语言
客户端/服务客户端服务端都可侧重客户端
拓展名.js.ts或.tsx
耗时更快编译代码需要些时间
数据绑定没有类型和接口的概念使用类型和接口表示数据
语法所有的语句都写在脚本标签内,浏览器将脚本标签内的文本识别为脚本一个TypeScript程序由模块、方法、变量、语句、表达式和注释构成
静态类型js中没有静态类型的概念支持静态类型
模块支持不支持模块支持模块
拓展名.js.ts或.tsx
接口没有接口支持接口
可选参数方法不支持支持
原型没有这种特性支持原型特性

2.ts中any和unknown有什么区别?

unknown和any的主要区别是unknown类型会更加严格;在对unknown类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对any类型的值执行操作之前,我们不必进行任何检查。

3.如何将unknown类型指定为一个更具体的类型?

  • 使用typeof进行类型判断
  • 对unknown类型使用类型断言,注意地,断言错了时语法能通过检测,但是运行的时候就会报错了。

4.说说对TypeScript中命名空间与模块的理解?区别?

  • 模块:与es6一样,任何包含顶级import或export的文件都被当成一个模块。如果一个文件不带有顶级的import或export声明,那么它的内容就视为全局可见的。
  • 命名空间:定义了标识符的可见范围,主要目的就是解决重名问题

区别:命名空间和模块可以包含代码和声明,不同的是模块可以声明它的依赖。

5.使用ts实现一个判断入参是否是数组类型的方法

function isArray(x: unknown): boolean { 
     if (Array.isArray(x)) {
      return true;  
     }  
     return false; 
 }

6.tsconfig.json文件有什么用?

tsconfig.json文件是JSON格式的文件,可以指定不同的选项来告诉编译器如何编译当前项目。

7.TypeScript支持的访问修饰符有哪些?

  • public:类的所有成员、其子类以及该类的实例都可以访问
  • protected:该类及其子类的所有成员都可以访问它们,但是该类的实例无法访问。
  • private:只有类的成员可以访问它们。

如果未指定访问修饰符,则它是隐式公共,因为它符合js的便利性。

8.TypeScript的主要特点是什么?

  • 跨平台:ts编译器可以安装在任何操作系统上
  • ES6特性
  • 面向对象语言:ts提供所有标准的OOP功能,如类、接口和模块。
  • 静态类型检查:ts使用静态类型并帮助在编译时进行类型检查。
  • 可选的静态类型:如果你习惯了js的动态烈性,ts还允许可选的静态类型
  • DOM操作:你可以使用ts操作DOM以添加或删除客户端网页元素。

9.TypeScript中的方法重写是什么?

如果子类具有与父类中声明相同的方法,则称为方法覆盖。换句话说,在派生类或子类中重新定义基类方法。

  • 方法必须具有与父类相同的名称
  • 该方法必须具有与父类相同的参数。
  • 必须有一个IS-A关系(继承)。

10.什么是TypeScript映射文件?

TypeScript Map文件是一个源映射文件,其中有关我们原始文件的信息。

  • .map文件是源映射文件,可让工具在发出的js代码和创建它的ts源文件之间进行映射。
  • 许多调试器可以使用这些文件,因此我们可以调试TypeScript文件而不是JavaScript文件。

11.TypeScript中的类型有哪些?

  • 内置:包括数字、字符串、布尔值、无效(void)、空值null和未定义undefined
  • 用户定义:枚举、类、接口、数组、元组

12.如何检查ts中的null和undefined?

var a: number;  
var b: number = null;  
function check(x, name) {  
    if (x == null) {  
        console.log(name + ' == null');  
    }  
    if (x === null) {  
        console.log(name + ' === null');  
    }  
    if (typeof x === 'undefined') {  
        console.log(name + ' is undefined');  
    }  
}  
check(a, 'a');  
check(b, 'b');  
//"a == null"  
//"a is undefined"  
//"b == null"  
//"b === null" 

13.ts中什么是类类型接口?

  • 如果接口用于一个类的话,那么接口会表示行为抽象
  • 对类的约束,让类去实现接口,类可以实现多个接口
  • 接口只能约束类的公有成员,无法约束私有成员、构造函数、静态属性/方法。

14.TypeScript中never和void的区别

  • void表示没有任何类型(可以被赋值为null和undefined)
  • never表示一个不包含值的类型,即表示永远不存在的值。
  • 拥有void返回值类型的函数能正常运行。拥有never返回值类型的函数无法正常返回,无法终止,或会抛出异常。

15.TypeScript中interface和type的差别是什么?

  • 相同点:都可以描述一个对象或者函数;都允许拓展,并且两者并不是相互独立也就是说interface可以extends type,type也可以extends interface
  • 不同点:type可以声明基本类型别名,联合类型,元组等类型;type语句中还可以使用typeof获取实例的类型进行赋值;interface能够声明合并。

16.TypeScript中泛型是什么?

是提供创建可重用组件的方法的工具,它能够创建可以使用多种数据类型而不是单一数据类型的组件。而且,它在不影响性能或生产率的情况下提供了类型安全性。泛型允许我们创建泛型类、泛型函数,泛型方法和泛型接口。

17.TypeScript中的getter/setter是什么?你如何使用它们?

Getter和Setter是特殊类型的方法,可帮助你根据程序的需要委派对私有变量的不同级别的访问。Getters允许你引用一个值但不能编辑它。Setter允许你更改变量的值,但不能查看其当前值。

class Employee { 
   private _fullName: string = ""; 
   get fullName(): string { 
     return this._fullName;  
   }
   set fullName(newName: string) { 
       this._fullName = newName; 
   }
}

18.TypeScript中什么是装饰器,它们可以应用于什么?

装饰器是一种特殊的声明,它允许你通过使用@<name>注释标记来一次性修改类或类成员。每个装饰器都必须引用一个将在运行时评估的函数。

19.解释如何使用TypeScript mixin?

本质上是在相反方向上工作的继承,Mixins允许你通过组合以前类中更简单的不分类设置来构建新类。

20.TypeScript中的类型断言是什么?

TypeScript中的类型断言的工作方式类似于其他语言中的类型转换,但没有C#和Java等语言中可能的类型检查或数据重组。类型断言对运行时没有影响,仅仅由编译器使用。类型断言本质上是类型转换的软版本,它建议编译器将变量视为某种类型,但如果它处于不同的形式,则不会强制它进入该模型。

21.为什么推荐使用TypeScript?

  • 简化js代码,使其更容易阅读和调试。
  • 是开源的。
  • 为js ide和实践提供了高效的开发工具。
  • 提供了es6的所有优点,以及更高的生产率
  • 对代码进行类型检查,可以帮助我们避免在编写js时经常遇到的令人痛苦的错误。
  • 强大的类型系统,包括泛型
  • 按照es5和es6标准编译,以支持最新的浏览器。
  • 与es对齐以实现兼容性。
  • 以js开始和结束
  • 支持静态类型
  • 是es3、es5和es6的超集。

22.什么是JSX?我们可以在TypeScript使用jsx吗?

JSX是带有不同扩展名的JavaScript,JSX是一种可嵌入的类似xml的语法。它将被转成js,要使用jsx,需要使用.tsx扩展名命名文件、启用jsx选项

23.什么是Rest参数?

rest参数用于向参数传递零个或多个值,它是通过在参数前加上(...)来声明的。它允许在函数不使用arguments对象的情况下拥有可变数量的参数。

rest参数要遵循的规则:

🍑 一个函数只允许一个rest参数

🍑 必须是数组类型

🍑 必须是参数列表中的最后一个参数

24.如何从任何.ts文件生成TypeScript定义文件?

可以使用tsc编译器从任何.ts文件生成TypeScript定义文件

tsc --declaration file1.ts

25.聊聊你对TypeScript类型兼容性的理解?

  • ts类型兼容:当一个类型 Y 可以赋值给另一个类型 X 时, 我们就可以说类型 X 兼容类型 Y。也就是说两者在结构上是一致的,而不一定非得通过 extends 的方式继承而来。
  • 接口的兼容性:X = Y 只要目标类型 X 中声明的属性变量在源类型 Y 中都存在就是兼容的( Y 中的类型可以比 X 中的多,但是不能少)
  • 函数的兼容性:X = Y Y 的每个参数必须能在 X 里找到对应类型的参数,参数的名字相同与否无所谓,只看它们的类型(参数可以少但是不能多。与接口的兼容性有区别)

26.协变、逆变、双变和抗变的理解?

  • 协变X=Y Y类型可以赋值给X类型的情况。也可以说是X类型兼容Y类型
interface X { name: string; age: number; } 
interface Y { name: string; age: number; hobbies: string[] }
let x: X = { name: 'xiaoming', age: 16 }
let y: Y = { name: 'xiaohong', age: 18, hobbies: ['eat'] }
x = y
  • 双变methodY=methodX 函数X类型可以赋值给函数Y类型,因为函数Y在调用的时候参数是按照Y类型进行约束的,但是用到的是函数X的属性和方法。
let methodY: (y: Y) => void
methodY = (y) => { console.log(y.hobbies) }
let methodX: (x: X) => void
methodX = (x) => { console.log(x.name) }
methodY = methodX
  • 双变(双向协变)X = Y;Y = X父类型可以赋值给子类型,子类型可以赋值给父类型,既逆变又协变。(ts2.x 之前支持这种赋值,之后 ts 加了一个编译选项 strictFunctionTypes,设置为 true 就只支持函数参数的逆变,设置为 false 则支持双向协变
  • 抗变(不变):非父子类型之间不会发生型变,只要类型不一样就会报错.

27.declare,declare global是什么?

declare是用来定义全局变量、全局函数、全局命名空间 declare global为全局对象window增加新的属性。

declare global { 
   interface Window { 
        csrf: string; 
   }
}

28.keyof和typeof关键字的作用?

  • keyof 索引类型查询操作符: 获取索引类型的属性名,构成联合类型。\
  • typeof: 获取一个变量或对象的类型。

29.简述工具类型 ExcludeOmitMergeIntersectionOverwrite的作用?

Exclude<T, U> 从 T 中排除出可分配给 U的元素。

Omit<T, K> 的作用是忽略T中的某些属性。

Merge<O1, O2> 是将两个对象的属性合并。

Compute<A & B> 是将交叉类型合并。

Intersection<T, U>的作用是取T的属性,此属性同样也存在与U

Overwrite<T, U> 是用U的属性覆盖T的相同属性。

站在大佬的肩膀上,可以看的更远!

前端面试题宝典 | 50个最新TypeScript面试题合集 – TypeScript开发教程 | TypeScript TS「面试题及答案」不断更新 | Typescript在线编译器 | TypeScript装饰器 | 《TypeScript入门与实战》