你真的会typescript吗?

150 阅读1分钟

一、使用

1.typescript 和 javascript 的关系?

ts 是 js 的超集,是 js 开发过程中的辅助工具,类似 vue、react 等最终都会被编译成 js 代码;在 ts 环境下,你只能按照相关的规则进行代码书写;

2.typescript 有哪些好处?

类型约束、安全、类型提示、规范化等;ts 以安全为主,同时有强大的类型推断能力,例如某个函数在传参时,你清楚的知道应该传什么类型,规避参数错误;

3.typescript 运行在什么环境?

ts 不能直接运行在浏览器,需要安装 typescript 模块才能运行,typescript 模块将 ts 代码编译成 js 后,浏览器才能识别;

4.typescript 如何安装?如何运行?

全局安装

$ npm install typescript -g

全局使用

  • 全局安装好后,可在命令行使用 tsc 命令运行相关 ts 文件,例如:tsc index.ts ,就会编译出一个同名的 js 文件;
  • 如果你需要修改 ts 配置,命令行运行 tsc --init,就会生成一个 json 配置文件,执行修改配置,例如 target 修改编译后的 js 版本;详细的配置参考这里
  • 编译时默认每次都需要执行 tsc xxx.ts,你也可以运行 tsc xxx.ts -- watch 启动热更新

项目中安装(这里以 rollup 作为演示,你当然也可以用 webpack 或者 vite 等)

npm install typescript rollup rollup-plugin-typescript2 rollup-plugin-serve @rollup/plugin-node-resolve -D

项目中使用

1.根目录运行 tsc --init 初始化 ts 配置文件

2.package.json 中,运行脚本选择使用 rollup -cw

-cw 的意思是? c : 使用 rollup.config.js 的配置,w : watch 实时观测 热更新

"scripts": {
	"dev": "rollup -cw"
},

3.根目录创建 rollup.config.js 文件,并进行相关配置,如果不会 rollup,建议仔细阅读代码注释

import { nodeResolve } from "@rollup/plugin-node-resolve";
import ts from "rollup-plugin-typescript2";
import serve from "rollup-plugin-serve";
import path from "path"; // node的path模块
export default {
  input: "src/index.ts", // 入口文件地址
  output: {
    file: path.resolve(__dirname, "dist/bundle.js"), // 打包编译后的文件
    format: "iife", // global cjs esm iife umd
    sourcemap: true, // 生成map文件 方便源码调试
  },
  plugins: [
    // 让node帮你读取ts文件
    nodeResolve({
      extensions: [".js", ".ts"], // 指定需要读取的文件类型
    }),
    ts({
      tsconfig: path.resolve(__dirname, "tsconfig.json"), // 读取前面生成的配置文件
    }),
    serve({
      port: 3000, // 编译完成后 启动一个服务 端口3000
      contentBase: "", // 为空表示从根目录开始查找
      // 打开指定的html,在该html中引入上面output的出口文件dist/bundle.js,就能愉快的玩耍了
      openPage: "/public/index.html",
    }),
  ],
};

5.如何在 vscode 中快速运行 typescript?

1 安装 ts-node,npm install ts-node -g

2 安装 vscode 编辑器插件 code renner,同时确保安装了全局的 typescript

3.选中相应的代码后,右键选择 run code 运行

6.自动化构建中使用 typescript,可能需要用到哪些插件?

  • typescript:这个是局部的
  • rollup:自动化构建工具
  • rollup-plugin-typescript2:让 rollup 能够编译 ts 的插件
  • rollup-plugin-serve:启动服务,类似于 webpack 的 dev-server
  • @rollup/plugin-node-resolve:rollup 的 node 解析插件,需要它去读取 ts 文件

7.tsconfig 中有哪些常用配置?

{
  "extends": "./paths.json",
  "compilerOptions": {
    "target": "esnext", // 指定ECMAScript版本 es5/es6等
    "module": "esnext", // 指定生成哪个模块系统代码 node/amd/commonjs/esnext等
    "strict": true, // 严格模式
    "jsx": "react-jsx", // 在tsx文件里面支持jsx,可选 react/preserve/react-jsx
    "moduleResolution": "node", // 如何处理模块 这里用node 默认Classic
    "skipLibCheck": true, // 忽略所有.d.ts的文件
    "esModuleInterop": true, // 支持使用import方式引入commonjs包
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
    "forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不一致的引用。
    "useDefineForClassFields": true, // 是否将class中赋值语义从set变更为Object.defineProperty
    "sourceMap": true, // 生成相应的 .map文件。
    "removeComments": true, // 编译时是否删除注释
    "strictNullChecks": true, // 是否强制检查null类型
    "noImplicitAny": true, // any值是否必须指定类型
    "noUnusedLocals": false, // 是否检测声明但没使用的变量
    "noUnusedParameters": false, // 是否检测声明但没使用的函数
    "allowJs": true, // 是否编译js文件
    "noFallthroughCasesInSwitch": true, // 严格校验switch case语法
    "resolveJsonModule": true, // 允许导入json模块
    "isolatedModules": false, // 是否必须有export
    "noEmit": true, // 不生成输出文件
    // 编译过程中需要引入的库文件的列表。
    "lib": ["dom", "dom.iterable", "esnext"]
  },
  // 指定需要编译的文件
  "include": ["src/**/*.ts", "src/**/*.tsx"]
}

二、基础

1.基础约束类型有哪些?

number、string、boolean、数组、元组、any、never、void、null、undefined、枚举、symbol、bigint 等

2.Number 和 number 有什么区别?String 和 string、Boolean 和 boolean?

number 是 ts 的类型,Number 是 js 的构造函数,Number 在 ts 中也可作为描述实例的类,因为 ts 默认做了拆箱处理,但是如果使用了 new 关键字后,Number 的实例就必须是 Number,而不能是 number,String 和 string 以及 Boolean 和 boolean 同理

3.什么是装箱?什么是拆箱?

装箱:给某个数据,装上它原本没有的属性

"zyr".split("");
// 'zyr'是基础类型数据,并不是对象,按照常理它并不能通过.xxx访问原型方法,
// 但实际它又可以访问,其实js帮我们做了一件事:String('zyr'),让其拥有对象的能力

拆箱:把某个数据才出来使用

new Number(10) + 10; // 20
// new Number是非标准特殊对象,并不是原始值,但是能参与原始值运算
// 同样也是js帮我们做了拆箱操作(new Number(10)).valueOf(),把数据拆出来用

4.元组能添加数据吗?能通过索引添加数据吗?

元组是数组的类型约束,具备数组的原型方法,可以通过 push shift 等添加删除数据,但同时只能添加元组中规定的类型

const a: [boolean, string] = [false, "1"];
a.push(true); // 可以添加 但是只能添加规定的类型
// a.push(1); // 报错 类型“1”的参数不能赋给类型“string | boolean”的参数
a.pop(); // 可以删除

元组不能通过无效的索引添加或修改数据

const a: [boolean, string] = [false, "1"];
a[2] = "1"; // 报错 长度为 "2" 的元组类型 "[boolean, string]" 在索引 "2" 处没有元素。

5.元组的使用场景?

数据交换、数组约束等

const changeArr = <T, K>(arr: [T, K]): [K, T] => {
  return [arr[1], arr[0]];
};
const a = changeArr<boolean, number>([false, 123]);

6.常用的枚举有哪几种?

普通枚举,如果不赋值,默认值是索引

enum A {
  A,
  b,
  c,
}

异构枚举,number 和 string 都有

enum A {
  A = 1,
  b,
  c = "3",
}

常量枚举,就是有明确赋值的

enum A {
  a = 10,
  b = 20,
  c = 30,
}

数值型枚举 ,和上面一样

字符串枚举 ,和上面差不多 改成 string 就行

7.什么是枚举的反举?

默认产生反举的两个条件:1 枚举是索引,2 不加 const

// 编译前
enum ROLE {
  A,
  B,
  C,
} // 大写是规范
// 编译后,会生成一个对象,对象中包含了枚举和反举
var ROLE;
(function (ROLE) {
  ROLE[(ROLE["A"] = 0)] = "A";
  ROLE[(ROLE["B"] = 1)] = "B";
  ROLE[(ROLE["C"] = 2)] = "C";
})(ROLE || (ROLE = {}));
// 打印结果 如果枚举的变量不赋值,默认枚举的就是索引,同时会产生索引的反举
// {"0":"A","1":"B","2":"C","A":0,"B":1,"C":2}

// 反举自动推断
enum ROLE {
  A = 2,
  B,
  C,
} // 编译前
// 编译后
var ROLE;
(function (ROLE) {
  ROLE[(ROLE["A"] = 2)] = "A";
  ROLE[(ROLE["B"] = 3)] = "B"; // 自动推断 变成了3
  ROLE[(ROLE["C"] = 4)] = "C"; // 自动推断 变成了4
})(ROLE || (ROLE = {}));

8.枚举加 const 和不加有什么区别?

不加 const 编译后会生成一个对象,参考上面;加 const 后就什么也没有,性能更好;

9.严格模式和非严格模式下,null 和 undefined 有什么区别?

在 ts 中 null 和 undefined 时 任何类型的子类型,严格模式下,null 只能赋值 null,undefined 只能赋值 undefined

而非严格模式下,null 可以时 undefined

// "strict": false,
const n: undefined = null;
const u: null = undefined;
// "strict": true,
const n: undefined = null; // 报错
const u: null = undefined; // 报错

10.never 场景?

关键描述:从不、代码无法达到终点、无法执行到结尾;"是任何类型的子类型"

场景:出错 、 死循环 、永远走不到的判断

// 永远走不到的判断
function setVal(val: string) {
  if (typeof val !== "string") {
    console.log(val); // never 帮我们代码做完整校验 永远走不到这里 val就是never
  }
}
// 抛出错误
function throwError(): never {
  throw new Error();
}
// 死循环
function whileTrue(): never {
  while (true) {}
}

11.void 的值能赋予哪些类型?严格模式下和非严格模式下有什么区别?

关键描述:从不、代码无法达到终点、无法执行到结尾;"是任何类型的子类型"

void 的值能赋予的类型:null、undefined

function fn(x: void): void {
  return null; // 非严格模式
}

严格模式下和非严格模式下有什么区别?:严格模式下,void 只能是 undefined 不能是 null

function fn(x: void): void {
  // return null; // 报错
  // return;
  // return undefined
}

12.声明变量的时候,如果不约束类型,也不赋值,会被默认推断成什么类型?

推断成 any,也就失去了类型约束,所以声明变量的时候,最好还是加上类型;

13.联合类型在约束了类型,但是没有赋值之前,能调用约束类型中的哪些方法?

能调用约束的类型中,可能共同拥有的方法

let numOrStr: string | number;
numOrStr.toString(); // string和number共同的 可以调
numOrStr.valueOf(); // string和number共同的 可以调
// numOrStr.toLowerCase() // string的 不可以调

14.断言有哪几种?

非空断言! 和 类型断言 as

非空断言:这个属性一定有,出了事找我

let a: string | number | undefined;
// 非空断言 表示这个东西一定有值,告诉ts 按照我的想法来,如果后续出错我负责, 一定不为空  ts特有
a!.toString(); // 这个一定有toString 我说的!
let b: number[];
b![0] = 1; // 他一定是数组 我说的!

类型断言:这个属性就是 as 的类型,我说的!

// 原本可能是string、number、undefined
let a: string | number | undefined;
a as string; // 但这里我说它一定是string
a as any as boolean; // 强转成boolean -- 这样容易出问题

15.断言的使用场景?

1.联合类型的断言判断,如上面代码所示

2.某些你能确定一定有值的场景下,可减少条件判断

const d = document.getElementById("app")!;
const e = document.getElementById("app");
e!.appendChild;

16.unknown 和 any 的区别?

unknown 是 any 的安全类型,一句话:就是比 any 要更安全,具体安全在哪里,参考下面的问题 17;

小知识:vue 源码最开始用的 any,后面基本全部改为 unkonwn

17.unknown 有哪些特点?

  • 不能通过属性变量取值
let u: unknown = 1;
// u.xxx; // ts报错
let u1: any = 1;
u1.xxx; // any可以访问 但是js会报错
  • unknown & 其它类型,都是其它类型
type x = boolean & unknown; // x = boolean
  • unknown | 其它类型,都是 unknown
type x = boolean | unknown; // x = unknown
  • unknown 的子集是 never,any 的子集是 string | number | symbol
type a = keyof any; // a = string | number | symbol
type u = keyof unknown; // u = unknown

三、函数和类

1.对函数的约束有哪些?

参数、返回值、函数本身

参数和返回值

// 函数关键字 写完后会对当前函数 自动推断类型,括号后面的是返回值类型
function sum1(x: number, y: string): string {
  return x + y;
}

函数自身

// 声明一个函数类型
type IFn = (a: number, b: number) => number;
const sum2: IFn = (a, b) => a + b;

2.什么是函数重载?以及使用场景?

一个函数可能会有多种不同的参数,以及返回值,需要根据不同的情况,定义多个函数类型对函数进行约束

使用场景:例如 假设函数根据接收的参数类型,返回相应的数据;

function toArray(value: string): string[]; // 重载 传入的是string 就返回string类型的数组
function toArray(value: number): number[]; // 重载 传入的是number 就返回number类型的数组
function toArray(value: string | number) {
  if (typeof value == "string") {
    return value.split("");
  } else {
    return value
      .toString()
      .split("")
      .map((item) => Number(item));
  }
}

3.class 的 constructor 中使用 this.xxx 赋值前,xxx 属性是否需要先声明?

需要预先声明,因为赋值的时候,并不知道它是什么类型的数据

4.class 的 constructor 中如何在接收属性的时候就赋值到实例上?

在属性的前面,加 public 关键字

class A {
  constructor(public a: number, public b: number) {
    // this.a = a; // 这一步可写可不写 不写的话上面会自动赋值
    // this.b = b; // 这一步可写可不写 不写的话上面会自动赋值
  }
}
const a = new A(1, 2);

5.class 的 static 和 public 有什么区别?

static 是 js 的 public 是 ts 的,static 的属性属于类本身,需要用类.xxx 调用,public 是实例的数据

class A {
  constructor(public a: number, public b: number) {}
  static get fn() {
    return 1;
  } // 这是es6的写法
  static fn1() {} // 只能用A.Fn1调用 es7可以直接用
  public fn2() {} // 实例属性,通过实例调用
}
const a = new A(1, 2);
a.fn1; // 无法访问,fn1是A的不是a的
A.fn1;
a.fn2;

6.es6 和 es7 的 static 有什么区别?

es6 需要在属性前加 get:static get name(){ },es7 可以直接用:static name(){},参考上面的代码

7.class 修饰符有哪些?分别有什么特点?

  • private:私有的,只能在 class 内部使用,也无法被继承
  • public:公开的,能在实例上使用,也能在 class 内部使用
  • protected:受保护的,只能在 class 内部使用,也可以在继承的子类中使用
  • readonly:只读的,不能修改
  • abstract:抽象的,没有具体实现,不能被 new

8.class 的 constructor 或属性前面加 protected 会怎样?

该 class 无法被 new,属性也不会出现在实例上;但是它们都可以被继承。

9.class 的 constructor 或属性前面加 private 会怎样?

该 class 无法被 new,也无法被继承,属性只能自己能用。

10.class 属性前加 readonly 会怎样?

属性只读,无法修改

11.class 的子类中如何调用父类的方法或者父类的属性?

通过 super 关键字,super.xxx 使用

12.什么是抽象类?有什么特点?

抽象类没有具体的实现,也就是说它无法被 new,只能被继承,一般用作基础类封装

13.非抽象类中,能使用抽象属性或方法吗?

不能,抽象属性只能在抽象类中使用!

14.什么是 class 装饰器?有什么特点?

一个实验性语法, 后面会有改动,vue2 刚开始用的就是装饰器

装饰器作用:扩展类、扩展类中的属性和方法

特点:不能修饰函数,函数会有变量提升的问题;可以多个装饰器叠加;必须写在 class 或属性名 的上面,使用装饰函数前面加上@符号

缺陷:class 的类型推断是有问题的,无法自动推断,this 上面的属性也不知道哪个会被用到哪个不会被用到,也就无法 tree-shaking,所以装饰器后面可能不会怎么用了。

function sey(str: string) {
  console.log(str);
}

@sey("哈哈")
class A {}

const a = new A(); // 打印 哈哈

15.class 装饰器的调用和执行过程?

以栈的形式,从上到下调用,从下到上执行;类似 compose 函数,洋葱模型

function addSay1(val: string) {
  console.log(val); // 1
  return function (target: any) {
    console.log(1); // 6
  };
}
function addSay2(val: string) {
  console.log(val); // 2
  return function (target: any) {
    console.log(2); // 5
  };
}
function addSay3(val: string) {
  console.log(val); // 3
  return function (target: any) {
    console.log(3); // 4
  };
}

@addSay1("11")
@addSay2("22")
@addSay3("33")
class A {
  constructor(public a: number, public b: number) {}
}
const a = new A(1, 2);
console.log(a);

16.装饰函数中,返回的函数可以接收哪些参数?

target、key、descriptor,也就是 defineProperty 接收的那几个

@eat // 修饰类本身
class Person {
  @Enum(false) // 修饰类的实例
  @toUpperCase // 修饰属性
  public name: string = "aa";

  @double(3) // 修饰静态属性
  static age: number = 18;

  // 还能修饰参数
  drink(@params content: any) {}
}
function eat(target: any) {
  target.prototype.eat = function () {
    console.log("eat");
  };
}

function toUpperCase(target: any, key: string) {
  let val: string = "";
  Object.defineProperty(target, key, {
    get() {
      return val.toUpperCase();
    },
    set(newVal: string) {
      val = newVal;
    },
  });
}

function double(num: number) {
  return function (target: any, key: string) {
    let v = target[key];
    Object.defineProperty(target, key, {
      get() {
        return num * v;
      },
    });
  };
}

function Enum(bool: boolean) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = bool;
  };
}

function params(target: any, key: string, index: number) {
  console.log(key, index);
}

四、interface 和 type

1.type 和 interface 的区别?什么时候用 type?什么时候用 interface?

type 通常用于描述某个类型,可能是普通类型,也可能是复杂类型,可以使用联合类型;不能添加新的类型等

interface 通常用于述对象的形状和结构,无法描述基本类型,无法使用联合类型;可以添加新类型;同名的会自动合并

选择用哪个,取决于个人习惯;定义类型别名用 type,定义对象用 interface,

另外看情况,假如你需要在函数上加属性,那么 interface 更适合,如果你只是定义函数的参数和返回值,type 更简洁;如果你需要使用联合类型,那么只能用 type;

2.type 能被继承吗?interface 能使用联合类型吗?

type 通过& 符号能实现合并;interface 无法使用联合类型;

type B = {
  b: number;
};
type C = {
  c: string;
};
type D = B & C;

3.如何手动对某个对象做类型反推?

可以使用 typeof

const obj = { a: 1, b: "2" };
type Obj = typeof obj;

4.如何用 interface 和 type 描述函数?

interface Fn1 {
  (x: number, y: number): number;
  id: number; // 除了描述函数,还能额外增加属性
}

type Fn2 = (x: number, y: number) => number;

const fn1: Fn1 = (x, y) => x + y;
fn1.id = 1; // 不写就报错
const fn2: Fn2 = (x, y) => x + y;

fn1(1, 2);
fn2(3, 4);

5.如何在已有接口中增加新的属性?

有以下几种实现方式:

1.类型断言,不推荐

interface A {
  color: string;
}

const a: A = {
  color: "red",
  title: "我就要写这个,管你用不用",
} as A;
// a.title // 取不到值,也没有提示

2.接口合并 或类型合并

interface A {
  color: string;
}
interface A {
  title: string;
}
type B = A & { title: string };

const a: A = {
  color: "red",
  title: "两个同名的会合并",
};

3.接口继承

interface A {
  color: string;
}
interface B extends A {
  title: string;
}

const b: B = {
  color: "red",
  title: "继承的A",
};

4.可选属性

interface A {
  color: string;
  [key: string]: any;
}
const a: A = {
  color: "red",
  title: "any",
};
a?.title;

6.如何实现 任意接口 和 可索引接口?

任意接口,就是上面的可选属性的写法,可索引接口将 key 改为 nunber 即可

interface A {
  [key: number]: number;
}
const a: A = [1, 2, 3];

7.如何从接口中取出某个属性作为新的类型?

用对象的 obj['key']取值的方式直接取就行了,注意 key 是 string

interface A {
  a: string;
  b: number;
  c: {
    x: () => void;
  };
}
type C = A["c"];

8.接口如何被 class 实现?

使用 implements 关键字

interface ISpeakable {
  name: string;
  speak(): void;
}

class Speak implements ISpeakable {
  public name = "1";
  speak(): void {
    throw new Error("Method not implemented.");
  }
}

9.一个 class 如何使用多个接口作为类型约束?

使用 implements 加逗号拼接

interface ISpeakable {
  name: string;
  speak(): void;
}
interface IChineseSpeakable {
  speakChinese(): void;
}

class Speak implements ISpeakable, IChineseSpeakable {
  public name = "1";
  speak(): void {
    throw new Error("Method not implemented.");
  }
  speakChinese(): void {
    throw new Error("Method not implemented.");
  }
}

10.接口如何继承?type 如何合并?

接口用 extends 关键字,type 用&

interface A {
  color: string;
}
interface B extends A {
  title: string;
}
type C = A & { name: string };

11.接口的属性修饰符有哪些?分别是什么作用?

可选属性:? 属性可选,非必传,取值的时候用 es6 可选链

只读属性:readonly

索引签名:[key : type]

动态属性:[ prop : string ] : type

12.如何用接口或 type 描述一个构造函数?

// 使用type描述一个可被new的构造函数
type IPerson = new (name: string) => Person;

// 使用interface描述
interface IPerson1 {
  new (name: string): Person;
}

class Person {
  eat() {}
  constructor(public name: string) {}
}

const p: IPerson = new Person("哈哈");

五、泛型

1.如何理解泛型?

调用的时候才确定类型,而不是一开始就确定类型;就类似一个变量,通过<>传递;函数、类、interface 都能接收泛型变量

function createArray<T>(num: number, value: T): Array<T> {
  let result = [];
  for (let i = 0; i < num; i++) {
    result.push(value);
  }
  return result;
}
let r = createArray<string>(5, "abc"); // r => string[]

2.泛型的使用场景?

某些函数,可能会在调用的时候,才能确定其参数类型和返回值类型;

也就是说,我们可以根据不同类型的参数,推断其返回值的类型,于这种类似的场景,都可以用泛型

function createArray<T>(num: number, val: T): T[] {
  let arr: T[] = [];
  for (let i = 0; i < num; i++) {
    arr.push(val);
  }
  return arr;
}

type A = { a: string; b: number };

const arr = createArray<string>(5, "abc");
const arr1 = createArray<A>(5, { a: "a", b: 1 });

3.如何在 interface 上使用泛型?

type IMyArr<T> = {
  [k: number]: T;
};

interface ICreateArray<K> {
  <T>(x: K, y: T): IMyArr<T>;
}

const createArray: ICreateArray<number> = <T>(num: number, val: T): T[] => {
  let arr: T[] = [];
  for (let i = 0; i < num; i++) {
    arr.push(val);
  }
  return arr;
};

type A = { a: string; b: number };

const arr = createArray<string>(5, "abc");
const arr1 = createArray<A>(5, { a: "a", b: 1 });

4.如何在 type 上使用泛型?

type ICreateArray<K> = <T>(x: K, y: T) => Array<T>;
const createArray: ICreateArray<number> = <T>(num: number, val: T): T[] => {
  let arr: T[] = [];
  for (let i = 0; i < num; i++) {
    arr.push(val);
  }
  return arr;
};

5.如何在 class 上使用泛型?

class MyNum<T> {
  public arr: T[] = [];
  push(num: T): void {
    this.arr.push(num);
  }
  get value(): T[] {
    return this.arr;
  }
}
const arr = new MyNum();
arr.push(123);
console.log(arr.value);

6.如何给泛型添加默认类型?

type Default<T = number> = { num: T };
const d: Default<number> = { num: 1 };
const d1: Default<string> = { num: "abc" };

7.interface 中如果有函数,泛型写在接口名上 和 加写在函数名上,有什么区别?

写在接口上,写在接口上是接口的泛型变量,写在函数上表示是函数的泛型变量

// K是属于interface的,T是属于函数的
interface ICreateArray<K> {
  <T>(x: K, y: T): Array<T>;
}

8.泛型如果不传参,会被推断成什么类型?

unkown

const fn = <T, K>(a: T, b: K): void => {};
fn(); // fn: <unknown, unknown>(a: unknown, b: unknown) => void

9.多个泛型的使用场景?

当多个属性需要在使用的时候才确定其具体类型时,就需要用多个泛型变量

const getVal = <T extends object, K extends keyof T>(obj: T, key: K) => {
  return obj[key];
};
getVal({ a: 1 }, "a");
getVal({ a: 1 }, "b"); // 报错

10.元组如何用泛型做类型交换?

使用多个泛型变量

const fn = <T, K>(arr: [T, K]): [K, T] => {
  return [arr[1], arr[0]];
};

const arr = fn([1, 2]);

11.什么是泛型约束?

要求类型中必须包含某个属性,使用关键字 extends

type withLen = { length: number };
// 约束其属性上必须有length
const computeArrayLength = <T extends withLen, K extends withLen>(
  arr1: T,
  arr2: K
): number => {
  return arr1.length + arr2.length;
};
computeArrayLength("123", { length: 3 });

12.泛型的两个参数,做数学运算时,会有什么问题?

数学运算时,可能会发生隐式转换,最终的类型无法推断

const sum = <T extends string>(a: T, b: T): T => {
  return (a + b) as T; // 得出的类型无法推断,所以需要断言
};

13.假设对象泛型 T,属性泛型 K,如何确保 K 是 T 的属性?

使用关键字 extends + keyof

const fn = <T extends object, K extends keyof T>(obj: T, key: K): T[K] =>
  obj[key];

14.泛型中 extends、keyof、typeof 分别有什么作用?有什么需要注意的?

extends 用于约束一个泛型变量必须包含哪些属性;可以多,但是不能少! keyof 用于从泛型变量中取出某个属性; typeof 是 js 的,可以判断其是否属于某个类型,或者用作类型反推;

15.实现一个 Array 的泛型?

注意:只能用 interface,因为 ts 内置了一个 Array 且是 interface 写的; 如果你用 type,会报同名错误;用 interface 会合并;

interface Array<T> {
  length: number;
  toString(): string;
  push(val: T): void;
  pop(): T | undefined;
  [index: number]: T;
  // ……
}

16.let xx = ClassA,ts 内部做了什么?

将 classA 作为了一个类型,并将类型赋予了实例 xx

17.typeof Class 和 Class 有什么区别?

typeof Class 返回的是 class 的类型 而 Class 是 typeof Class 的实现

18.fn(Animal),Animal 是一个 class,报错原因?

Animal 作为泛型变量传递给了函数的时候,相当于 Animal 作为了类型,函数的实参类型就被要求类型属于 Animal,而不是 Animal 本身; 就像你不能把 number 作为函数参数传递一样,number 是类型,而不是值!number 最终会被编译成空气!

19.type T = keyof any, T 的类型有哪些?

number | string | symbol

keyof unknown 返回的是 never

六、类型保护

1.什么是类型保护?

对不确定类型的数据,进行类型判断或者属性检查前,ts 无法推断出具体的类型,所以会将数据可能拥有的某些属性保护起来,不让你用;判断完类型后,ts 就能推断出数据的类型,从而将某些属性放开给你用;

let a: string | number;

if (typeof a! === "string") {
  a.toLowerCase;
}

2.类型保护有哪些检查方式?

typeof、instanceof、in

// 1.typeof 区分类型保护变量
function fn(val: string | number) {
  if (typeof val == "string") {
    val.match;
  } else {
    val.toFixed;
  }
}
// 2.instanceof
class Person {
  eat() {}
}
class Dog {}
const createClass = (clazz: new () => Person | Dog) => new clazz();
let r = createClass(Person);
if (r instanceof Person) {
  r.eat; // Person,能.出eat
} else {
  r; // Dog 没有任何功能
}
// 3.in语法
type Fish = { swiming: string };
type Bird = { fly: string };
function isFish(animal: Fish | Bird): animal is Fish {
  return "swiming" in animal;
}
function getAnimalType(animal: Fish | Bird) {
  if (isFish(animal)) {
    animal.swiming;
  } else {
    animal.fly;
  }
}

3.is 语法?有什么作用?

一个类型谓词,让 ts 收窄类型判断,用于在可能出现多种类型的判断中,指定具体的某个类型

例如某个函数的返回值依靠某个可能有多种类型的参数,返回值无法正确推断是什么类型,此时加上 is 指定参数的具体类型就能正确推断出返回值

// 指定其参数一定是string类型
function isString(val: any): val is string {
  return Object.prototype.toString.call(val) == "[object String]";
}

let str = 222;
if (isString(str)) {
  str; // never 走不到这里
}

4.实现一个有类型保护能力的类型检查方法?

上面 2 和 3 中的代码都可以用作参考

这里例举一个通过字面量类型来判断的案例

interface IButton1 {
  color: "blue";
  class: string;
}
interface IButton2 {
  color: "green";
  class: string;
}
function getButton(button: IButton1 | IButton2) {
  if (button.color == "blue") {
    button;
  } else {
    button;
  }
}

5.什么是完整性保护?

保护代码能够完整的运行; 主要靠的是 never,利用 never 无法到达最终结果的特性,来保证代码的完整

interface ISquare {
  kind: "square";
  width: number;
}
interface IRant {
  kind: "rant";
  width: number;
  height: number;
}
interface ICircle {
  kind: "circle";
  r: number;
}
const assert = (obj: never) => {
  throw new Error("err");
};
// 完整性保护 保证代码逻辑全部覆盖到
function getArea(obj: ISquare | IRant | ICircle) {
  switch (obj.kind) {
    case "square":
      // 在这里 你没法用height r,已经受到保护了
      return obj.width * obj.width;
    case "rant":
      // 在这里 你没法用r,已经受到保护了
      return obj.width * obj.height;
    case "circle":
      // 在这里 你没法用width height,已经受到保护了
      return obj.r;
    default:
      assert(obj);
  }
}
getArea({ kind: "circle", r: 10 });

七、交叉类型

1.ts 文件中没有任何导出时,其中声明的接口和类型等,会有什么变化?

会变成当前运行环境中,全局的接口和类型

2.两个同名的 interface 会发生什么?

会自动合并,自己去写一下试试

3.两个同名的 interface 中,同名属性的类型如果不一样,会发生什么?

会合并成 never

4.type 如何合并?合并后的同名属性如果类型不一样,会发生什么?

使用 & 进行合并,后面的同名属性 会覆盖掉前面的

5.interface 如何在原接口上,增加新的属性?

1.利用同名 interface 会合并的特性,再继续声明

2.使用 extends 继承

6.type 如何在原类型上增加新属性?

利用类型合并 & { newType:number }

7.实现一个对象合并函数,要求将对象类型也合并?

需要用到泛型

function mixin<T extends object, K extends object>(o1: T, o2: K): T & K {
  return { ...o1, ...o2 };
}

8.类型合并时有哪些需要注意的?

1.interface 同名属性如果类型不一样,会转为 never

2.type 同名属性如果类型不一样,后面的会替换前面的

……

八、兼容性

1.什么是鸭子类型?

国王需要 1000 只鸭子,但是只找来 999 只,剩下的一只无论如何也找不到,于是找来一只鹅放进去!

只要有 2 根翅膀,2 只脚,叫声像鸭子……等等,就可以认为它是一只鸭子!

用到数据中,就是只要它符合个类型中的某些特性,就可以认为它属于这个类型

2.基础类型的兼容性?

少的值可以赋给多的类型,有没有那么多不重要,鸡只要会叫就可以充当鸭子

type Ji = number | string; // 鸡 有number 和 string
let ny: Ji = "ny"; // 'ny'只有string类型,但只要有string就可以说它是只鸡

type Kun = { toString(): string };
let k: Kun = "鸡你太美"; // 多的条件可以赋予给少的条件

3.interface 的兼容性?

多的可以赋值给少的,但是少的不能赋值给多的

一切都是为了安全!

interface IVegetables {
  color: string;
  taste: string;
}
interface ITomato {
  color: string;
  taste: string;
  size: string;
}
let vegetables!: IVegetables;
let tomato!: ITomato;
vegetables = tomato; // 通过接口的兼容性 多的可以给少的
// tomato = vegetables; // 报错 少的不能给多的,参考问题2的,你可以说ny是鸡,但你不能说鸡是ny

4.type 属性少的可以赋值给属性多的吗?

不可以!和 niterface 一样的道理

type A = string | number;
type B = string;
let a!: A;
let b!: B;
// a = b; // 多的可以给少的
b = a; // 少的不能赋值给多的

5.函数兼容性有哪些?

参数兼容、返回值兼容

函数的参数个数类似于基础类型,少的可以给多的,但是多的不能少;就是说,假如限制了 2 个参数,你可以传一个传 2 个都可以,但是你不能传 3 个;

函数的返回值,只能多不能少,你可以比返回值要求的类型多,但是不能少!就类似 type 和 interface

6.什么是逆变?什么是协变?什么是反协变?

口诀:传逆父、反斜子

函数参数传递的时候,可以少不能多;函数返回值要求的内容,只能多不能少;

反协变就是返回值可以少不能多

class Parent {
  money!: string; // 父亲有钱
}
class Child extends Parent {
  house!: string; // 儿子有钱 还有房
}
class Grandson extends Child {
  car!: string; // 孙子有钱有房,还有车
}
// 接收的回调函数中,把儿子作为参数类型,返回的也是儿子的类型
function getFn(cb: (person: Child) => Child) {}

// 传递的参数要求是儿子,然而传父亲也可以兼容,但是不能传孙子
// 返回的数据要求是儿子,然而可以是孙子,但是不能是父亲
getFn((person: Parent) => new Grandson());
// getFn((person: Parent) => new Parent()); // 报错
// getFn((person: Grandson) => new Grandson()); // 报错
// getFn((person: Grandson) => new Parent()); // 报错

7.函数 A 的参数是函数 B,B 声明时的参数接收,和调用时的参数传递,有什么兼容性特点?

接收的可以多,调用的不能多!

另外,返回值时反着来的,参考前面的逆变和协变

// cb调用时,参数只能少,不能多!
const getType = (cb: (val: string | number) => string | number) => cb("123");
// cb接收时,单数只能多,不能少!
getType((val: string | number | boolean) => 123);

九、条件类型 & 内置方法

1.什么是 ts 条件类型?

三目运算 + extends,通过 extends 判断,返回不同的条件类型

type A = { a: number };
type B = { b: number };
type MyType<T> = T extends A ? A : B;

2.实现一个泛型方法,判断传入的类型,返回对应的类型?

interface Fish {
  name: string;
  type: "鱼";
}
interface Bird {
  name: string;
  type: "鸟";
}
interface Swiming {
  swiming: string;
}
interface Sky {
  sky: string;
}
type MyType<T> = T extends Bird ? Sky : Swiming;
type IEnv = MyType<Fish | Bird>;

3.实现一个泛型方法,判断传入的类型中是否存在某个属性,然后返回对应的类型?

type A = { a: number };
type B = { b: number };
type MyType<T> = T extends { a: number } ? A : B;
type C = MyType<A>;

4.实现一个泛型方法,从联合类型中排除某几个属性 (Exculde)

type Exculde<T, K> = T extends K ? never : T;
type A = Exculde<string | number | boolean, boolean>;

5.实现一个泛型方法,从联合类型中抽离某几个属性 (Extract)

type Extract<T, K> = T extends K ? T : never;
type A = Extract<string | number | boolean, boolean>;

6.实现一个泛型方法,在多个属性中排除 null 和 undefined (NonNullable)

type MyNonNullable<T> = T extends null | undefined ? never : T;
type A = MyNonNullable<string | number | boolean | null>;

7.实现一个泛型方法,获取某个函数的返回值类型? (ReturnType)

type MyReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer R
  ? R
  : void;
const fn = (): number | string => 1;
type A = ReturnType<typeof fn>;

8.实现一个泛型方法,获取某个函数的参数类型? (Parameters)

type MyParameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : any;
const fn = (a: number, b: string): void => undefined;
type A = MyParameters<typeof fn>;

9.实现一个泛型方法,获取某个构造函数的参数类型 (ContructorParameter)

class Person {
  constructor(name: string) {}
}
type MyConstructorParameters<T extends new (...args: any[]) => any> =
  T extends new (...args: infer CP) => any ? CP : any;
type C = MyConstructorParameters<typeof Person>;

10.实现一个泛型方法,获取某个构造函数的实例类型 (InstanceType)

class Person {
  constructor(name: string) {}
}
type MyInstanceType<T extends new (...args: any[]) => any> = T extends new (
  ...args: any[]
) => infer R
  ? R
  : any;
type Instance = MyInstanceType<typeof Person>;

11.实现一个泛型方法,将全部参数改为可选 (Partial)

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};
type D = MyPartial<{ name: string; age: number }>;

12.实现一个泛型方法,将全部参数改为必选 (Required)

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};
type D = MyRequired<{ name?: string; age?: number }>;

13.实现一个泛型方法,将全部参数改为只读 (Readonly)

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};
type D = MyReadonly<{ name?: string; age?: number }>;

14.实现一个泛型方法,从对象类型中抽离某几个属性 (Pick)

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};
type E = MyPick<{ name: string; age: number }, "name">;

15.实现一个泛型方法,从对象类型中排除某几个属性 (Omit)

// Exclude找到排除的类型,再用MyPick挑出排除的类型
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
type E = MyOmit<{ name: string; age: number }, "name">;

16.实现一个泛型方法,将一个类型的所有属性值都映射到另一个类型上并创造一个新的类型 (Record)

// 将K的属性映射到另一个类型P上,同时P的类型是T
type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};
interface Preson {
  name: string;
  age: number;
}
// type Names = "zs" | "ls" | "ww";
const enum Names {
  zs = "zs",
  ls = "ls",
  ww = "ww",
}
// 将Names上的属性作为key,将Preson作为key的类型
type Presons = MyRecord<Names, Preson>;
// 返回一个新的类型,这个经常会用到哦
const presons: Presons = {
  zs: { name: "张三", age: 18 },
  ls: { name: "李四", age: 19 },
  ww: { name: "王五", age: 18 },
};

十、自定义类型 & 命名空间 & 声明文件

1.实现一个能将对象进行装包和拆包的泛型方法,并实现对应的函数

装包:例如 vue3 的 ref 创建一个对象,对象上面有 value 属性

拆包:再 template 中使用 ref 的时候,可以不用.value,能直接用

let data = {
  name: "zf",
  age: 12,
};
// 对每个属性都加上get set
type Proxy<T> = {
  get(): T;
  set(value: any): void;
};
// 改写属性的类型
type Proxify<T extends object> = {
  [K in keyof T]: Proxy<T[K]>;
};
// 装包
function proxify<T extends object>(obj: T): Proxify<T> {
  let result = {} as Proxify<T>;
  for (let key in obj) {
    let value = obj[key];
    result[key] = {
      get() {
        return value;
      },
      set(newValue) {
        value = newValue;
      },
    };
  }
  return result;
}
let proxyDatas = proxify(data);

console.log(proxyDatas.name.get());
proxyDatas.name.set("xxx");
console.log(proxyDatas.name.get());

// 拆包
function unProxify<T extends object>(obj: Proxify<T>): T {
  let result = {} as T;
  for (let key in obj) {
    let value = obj[key];
    result[key] = value.get();
  }
  return result;
}
let data2 = unProxify(proxyDatas);
console.log(data2.name);

2.实现一个泛型方法,获取两个类型的差集

let person1 = {
  name: "zs",
  age: 12,
};
let person2: {
  age: 13;
};
// exclude:在一群类型中忽略掉某个类型, omit:对象中忽略
// 直接用Omit从T中忽略掉K 就是差集
type Diff<T extends object, K extends object> = Omit<T, keyof K>;
type myDiff = Diff<typeof person1, typeof person2>;

3.实现一个泛型方法,获取两个类型的交集

type Inter<T extends object, K extends object> = Pick<
  K,
  Extract<keyof T, keyof K>
>;
type myInter = Inter<typeof person1, typeof person2>;

4.实现一个泛型方法,获取两个类型的并集

type Merge<T extends object, K extends object> = Omit<T, keyof K> & K;

5.实现一个泛型方法,将类型展开,方便提示

type Compute<T> = { [K in keyof T]: Compute<T[K]> };

6.什么是命名空间?

为了保护变量不被污染,js 没有命名空间,以前都是用闭包保护变量做的命名空间,后面有了模块化规范

ts 中的命名空间使用关键字 namespace

namespace Home {
  export const a = 1;
}

7.module 和 namespace 有什么共特性?

module 也能产生命名空间,但是还是希望你写成 namespace, 后面有一种场景 只能使用 module 关键字

8.namespace 加 declare 和不加 declare 有什么区别?

加 declare 则是全局的,同时 namespace 中不用再 export

declare namespace Home {
  const a = 1;
}
Home.a; // 可以访问

namespace Home1 {
  export const a = 1;
}
Home1.a; // 属性要加export

namespace Home2 {
  const a = 1;
}
// Home2.a; // 无法访问 必须加export

9.ts 中的命名空间 有哪些特点?

1.不加 declare 时,属性必须 export

2.两个同名的 namspace 会合并,但是合并后如果有同名的变量会报错

3.命名空间可以无限嵌套

4.最终编译成 js,就是一个自执行函数

10.ts 在 commonjs 中使用时的约束有哪些?

commonjs 的 require 语法没有类型提示!

所以 ts 弄了一个兼容写法:import fs = require('fs');

11.不是 ts 写的第三方模块,如何做类型声明?

使用 declare 声明,前置条件如下: 1.tsconfig.json 中:

{
  "moduleResolution": "node",
  "baseUrl": "./", // 基础路径
  "paths": {
    "*": ["types/*"] // 基础路径下的 types下的全部.d.ts文件
  }
}

2.创建对应的.d.ts 文件, 以 Jqure 为例:jqure.d.ts

declare function $(selector: string): {
  css(val: object): void;
  height(val: string): void;
  width(val: string): void;
};
// 使用时
import jquery from "jquery";
jquery("#xxx").width("100");

12.如何声明全局类型?

步骤参考上面

declare global {
  interface Window {
    Bmap: any;
  }
}

13.如何在 String 的 prototype 上添加新的属性类型?

declare global {
  interface String {
    double(): string;
  }
}

String.prototype.double = function () {
  return "acbc";
};

14.如何对.vue 类型的文件,做类型声明?

declare module "*.vue" {
  const component: object;
  export default component;
}