TypeScript相关知识点

401 阅读18分钟

前端面试题系列文章:

【1】「2023」HTML基础知识点

【2】「2023」ECMAScript基础知识点

【3】「2023」CSS基础知识点

【4】「2023」计算机网络基础知识

【5】「2023」计算机网络-HTTP知识点

【6】「2023」浏览器相关知识点

【7】「2023」React相关知识点

【8】「2023」TypeScript相关知识点

【9】「2023」Webpack相关知识点

【10】「2023」代码输出结果相关知识点

【11】「2023」手动实现代码相关知识点

【12】「2023」性能优化相关知识点

【13】「2023」H5相关知识点

TypeScript是什么?

TypeScript是一种基于JavaScript构建的强类型编程语言。它是 JavaScript 的一个超集,本质上向这个语言添加了可选的静态类型和基于类的面向对象编程(如类、接口、继承、泛型)。TypeScript并不直接在浏览器上运行,需要编译器编译成纯JavaScript来运行。

TypeScript优缺点?

优点

  • 代码的可读性和可维护性: 显示的类型声明可以帮助我们理解代码的逻辑。比如后端的接口返回值,之前需要通过network或者文档查看,才能知道返回的数据结构。而用ts声明之后,只需要看ts声明就行。
  • 在编译阶段避免错误:在编译阶段会提示很多错误。比如访问了某个不存在的属性,访问某个可能为undefined类型变量的某个属性。
  • IDE自动填充,自动联想,提升开发体验:包括代码补全、接口提示、跳转到定义、重构等。

缺点

  • 学习成本: 需要理解接口(interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等在 JavaScript 中不存在的概念。
  • 会增加一些开发成本:在开发过程中,需要明确声明类型。
  • 一些JS库需要兼容:提供声明文件,像vue2,底层对ts的兼容就不是很好。
  • 编译速度:ts的编译是需要时间成本的,这就意味着项目大了之后,会影响体验。

Deno 内部代码将停用 TypeScript,并公布五项具体理由

TypeScript类型 - 基础类型

  • 常用:Boolean、Number、String、Array、Enum、Any、Void
  • 不常用:Tuple、Null、Undefined、Never

1. Boolean 类型

const isShow: boolean = fasle;

2. Number 类型

const count: number = 3;

3. String 类型

const str: string = 'zaoren';

4. Array 类型

const list: number[] = [1, 2, 3];
// or 使用泛型语法
const list: Array<number> = [1, 2, 3];

5. Enum 类型

enum Status {
  Processing = 1,
  Done = 2,
  Expired = 3,
}

// 我们可以这样使用,比起 activityStatus === 3 可读性更好
if (activityStatus === Status.Expired) {
    message.info('活动已过期');
}

6. Any 类型

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型称为了类型系统的顶级类型。

let anyType: any = '1324';
anyType = 666;
anyType = false;

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

7. UnKnown 类型

和 any 类型一样。所有的类型也都能赋值给 unknown 类型。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型。

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK

但是 unknown 类型只能赋值给 any 类型和 unknown 类型本身

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error

我们尝试对 unknown 类型的值执行操作时会发生什么。

let value: unknown;

value.foo; // Error
value.trim(); // Error
(value as {foo: string}).foo // OK

综上:unknown 类型和 any 类型的区别:unknown 类型和 any 类型类似。与 any 类型不同的是。unknown 类型可以接受任意类型赋值,但是 unknown 类型赋值给其他类型前,必须被断言。从一定程度上缓解了any 过于宽松的问题。

8. Tuple 类型

元组类型允许标识一个已知元素数量和类型的数组,各元素的类型不必相同。比如,你可以定义一对值分别为stringnumber类型的元组。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

9. Void 类型

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。通常用在一个函数没有返回值的情况。

function refresh(): void => {
  window.location.reload();
}

10. Null 和 Undefined 类型

TypeScript 里, undefinednull两者有个字的类型,分别为 undefinednull

let u: undefined = undefined;
let n: null = null;

默认情况下nullundefined是所有类型的子类型。就是说你可以把nullundefined赋值给number类型的变量。但是,通常情况下,我们不建议这么做,一般会在tsconfig.json中指定--strictNullChecks 标记,nullundefined 只能赋值给 void 和它们各自的类型。

注意,tsconfig.json 中如果strict为true的话,strictNullChecks 默认也为 true。

11. Never类型

Never 类型标识的是那些用不存在的值的类型。例如,never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。如:

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while(true) {}
}

在 TypeScript 中,可以通过 Never 类型来实现全面性检查,如下

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

注意在 esle 中的逻辑,是永远不会被执行到的,假设有一个,Foo类型被修改了:

type Foo = string | number |  boolean

然后却忘记了在 controlFlowAnalysisWithNever 函数中兼容 boolean 类型。那么,当 Foo 是一个 Boolean 类型时,就会将 Boolean 类型的值传给 never,这时就会产生编译错误。通过这个方式,我们可以确保逻辑的完整性。

使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

TypeScript类型 - 对象类型

TypeScript 的两种对象类型:interface 和 type

interfacetype是 TypeScript 中描述对象类型的两种方法。

// 使用 interface 声明对象
interface Hero {
  name: string;
  age: number;
  say(): string; // say函数返回值为string
  [propname: string]: any; // 当前Hero可定义任意字符串类型的key
}

// or  使用 type 声明对象
type Hero {
	name: string;
    age: number;
    say(): string; // say函数返回值为string
    [propname: string]: any; // 当前Hero可定义任意字符串类型的key
}

interface 和 type 的相同点:

  • 都可以描述对象或者函数
  • 都支持类型拓展(type通过类型交叉(Intersection Types),interface通过继承(extends))

interface 和 type 的不同点:

  • type 可以声明 基本类型,联合类型,元组,interface不行
// 基本类型
type Name = string;

interface Dog {
    wong();
}
interface Cat {
    miao();
}
// 联合类型
type Pet = Dog | Cat
// 元组类型
type PetList = [Dog, Pet]
  • type 可以使用 typeof 获取实例的类型进行赋值
// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div;
  • 多个相同的 interface 声明可以自动合并,多个相同的 type 声明会抛出错误
interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string
}
*/

type User = {
	name: string
  age: number
}

// Error: Duplicate identifier 'User'.
type User = {
	sex: string
}
  • interface 可以 extends 进行类型拓展,type 可以通过交叉类型进行类型拓展
// interface 通过 extends
interface Name {
  name: string;
}
interface User extends Name {
  age: number;
}
// type 通过交叉类型
interface Name {
  name: string;
}
type User = Name & {
  age: number;
}

什么时候用 type 什么时候用 interface?

官方只提到了 type 和 interface 的区别

  1. you cannot use implements on an class with type alias if you use union operator within your type definition

  2. you cannot use extends on an interface with type alias if you use union operator within your type definition

  3. declaration merging doesn’t work with type alias

但是从 tslint 的 interface-over-type-literal 这条规则来看,当两者皆可的时候,要优先使用 interface。因为 interface 支持 implements、extends、merge。

TypeScript类型 - 联合类型 && 交叉类型

联合类型

某个变量可能是多个 interface 中的一个,用|分割

const sayHello = (name: string | undefined) => {
  /* ... */
};

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给sayHello 函数。

sayHello("Semlinker");
sayHello();

交叉类型

交叉类型是将多个类型合并为一个类型,由&连接

interface IPerson {
  id: string;
  age: number;
}

interface IWorker {
  companyId: string;
}

type IStaff = IPerson & IWorker;

const staff: IStaff = {
  id: 'E1006',
  age: 26,
  companyId: '234234'
};

console.dir(staff)

TypeScript - 函数

参数类型和返回类型

function createUseId(name: string, id: number): string {
  return name + id;
}

函数类型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;

可选参数及默认参数

// 可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默认参数
function createUserId(
  name: string = "Semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。

函数重载

函数重载或方法重载是使用相同名称和不同参数类型或数量创建多个方法的一种能力。也就是为同一个函数提供多个函数定义类型来进行函数重载,编译器会根据这个列表去处理函数的调用。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: number | string, b: number | string): number | string {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}

TypeScript - 断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器:“相信我,我知道我自己在干什么”。类型断言好比其他语言里的类型转化,但是不进行特殊的类型检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:“尖括号”语法as语法

“尖括号”语法

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

as语法

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

TypeScript - 接口

在面向对象的语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。

但在 TypeScript 中的接口是一个非常灵活的概念,除了可以用于对类的一部分行为进行抽象以外,也长用于对”对象的形状(Shape)“进行描述。

接口对对象形状的描述

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

let zaoren: Person {
 	name: "weq",
    age: 25,
}

可选 && 只读属性

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

只读属性用户限制只能在对象创建的时候修改其值。此外 TypeScript 还提提供了 ReadonlyArray<T> 类型,它与 Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建以后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

TypeScript类型 - 泛型

泛型的概念

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

设计泛型的目的

在软件工程中,我们在设计时需要考虑可重用性。组件不仅能够支持当前的的数据结构,同时也能支持未来的数据结构。

像Java这样的语言中,可以通过使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间通过有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型接口

interface GenericIdentityFn<T> {
  	(arg: T): T;
}

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

泛型变量

对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。下面介绍一下一些常见泛型变量代表的意思。

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

TypeScript - 工具类型

编译上下文

tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径
  • 用于配置 TypeScript 编译器
  • 用于指定编译的文件

tsconfig.json 字段意义

  • files - 设置要编译的文件的名称
  • compilerOptions - 设置与编译流程相关的选项
  • include - 设置需要编译的文件,支持路径模式匹配
  • exclude - 设置无需进行编译的文件,支持路径模式匹配

compilerOptions 选项

compilerOptions支持很多配置。常见的有 baseUrltargetbaseUrlmoduleResolutionlib 等。

{
  "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          // 为装饰器提供元数据的支持
  }
}

Compiler Options 官方文档

实用技巧

typeof 关键字

typeof 关键词除了做类型保护,还可以从变量推出类型。

// 先定义变量,再定义类型
let p1 = {
  name: "hello",
  age: 10,
  gender: "male",
};
type People = typeof p1;
function getName(p: People): string {
  return p.name;
}
getName(p1);

keyof 关键字

keyof 可以用来取得一个对象接口的所有 key 值

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}
//type PersonKey = 'name'|'age'|'gender';
type PersonKey = keyof Person;

function getValueByKey(p: Person, key: PersonKey) {
  return p[key];
}
let val = getValueByKey({ name: "hello", age: 10, gender: "male" }, "name");
console.log(val);

索引访问操作符

使用 [] 操作符可以进行索引访问

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

type x = Person["name"]; // x is string

映射类型 in

在定义的时候用 in 操作符去批量定义类型中的属性(可以直接使用ts自带的Partial<T>

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}
//批量把一个接口中的属性都变成可选的
type PartPerson = {
  [Key in keyof Person]?: Person[Key];
};

let p1: PartPerson = {};

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

内置工具类型

  • Exclude<T,U> 从 T 可分配给的类型中排除 U
// 源码
type Exclude<T, U> = T extends U ? never : T;

type E = Exclude<string | number, string>;
let e: E = 10;
  • Extract<T,U> 从 T 可分配给的类型中提取 U
// 源码
type Extract<T, U> = T extends U ? T : never;

type E = Extract<string | number, string>;
let e: E = "1";
  • Partial,Partial 可以将传入的属性由非可选变为可选
type Partial<T> = { [P in keyof T]?: T[P] };
interface A {
  a1: string;
  a2: number;
  a3: boolean;
}
type aPartial = Partial<A>;
const a: aPartial = {}; // 不会报错
  • Required,Required 可以将传入的属性中的可选项变为必选项,这里用了 -? 修饰符来实现。
interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}
/**
 * type Required<T> = { [P in keyof T]-?: T[P] };
 */
let p: Required<Person> = {
  name: "hello",
  age: 10,
  gender: "male",
};
  • Readonly Readonly 通过为传入的属性每一项都加上 readonly 修饰符来实现。
interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}
//type Readonly<T> = { readonly [P in keyof T]: T[P] };
let p: Readonly<Person> = {
  name: "hello",
  age: 10,
  gender: "male",
};
p.age = 11; //error
  • Pick<T,K> Pick 能够帮助我们从传入的属性中摘取某些返回
interface Todo {
  title: string;
  description: string;
  done: boolean;
}
/**
 * From T pick a set of properties K
 * type Pick<T, K extends keyof T> = { [P in K]: T[P] };
 */
type TodoBase = Pick<Todo, "title" | "done">;

// =
type TodoBase = {
  title: string;
  done: boolean;
};
  • Record<K,T> 构造一个类型,该类型具有一组属性 K,每个属性的类型为 T。可用于将一个类型的属性映射为另一个类型。Record 后面的泛型就是对象键和值的类型。

简单理解:K 对应对应的 key,T 对应对象的 value,返回的就是一个声明好的对象 但是 K 对应的泛型约束是keyof any 也就意味着只能传入 string|number|symbol

// type Record<K extends keyof any, T> = {
// [P in K]: T;
// };
type Point = "x" | "y";
type PointList = Record<Point, { value: number }>;
const cars: PointList = {
  x: { value: 10 },
  y: { value: 20 },
};
  • Omit<K,T> 基于已经声明的类型进行属性剔除获得新类型
// type Omit=Pick<T,Exclude<keyof T,K>>
type User = {
id: string;
name: string;
email: string;
};
type UserWithoutEmail = Omit<User, "email">;// UserWithoutEmail ={id: string;name: string;}
};

TypeScript开发辅助工具推荐

TypeScript Playground

TypeScript 官方提供的在线 TypeScript 运行环境,利用它你可以方便地学习 TypeScript 相关知识与不同版本的功能特性。

TypeScript UML Playground

一款在线 TypeScript UML 工具,利用它你可以为指定的 TypeScript 代码生成 UML 类图。

JSON TO TS

一款 TypeScript 在线工具,利用它你可以为指定的 JSON 数据生成对应的 TypeScript 接口定义。

TypeScript AST Viewer

一款 TypeScript AST 在线工具,利用它你可以查看指定 TypeScript 代码对应的 AST(Abstract Syntax Tree)抽象语法树。

其他问题

ts 中的访问修饰符

  • public,任何地方
  • private,只能在类的内部访问
  • protected,能在类的内部访问和子类中访问
  • readonly,属性设置为只读

const和readonly的区别

  • const用于变量,readonly用于属性
  • const在运行时检查,readonly在编译时检查
  • 使用const变量保存的数组,可以使用push,pop等方法。但是如果使用ReadonlyArray<number>声明的数组不能使用push,pop等方法。

type 和 interface 的区别

  • type 可以声明 基本类型,联合类型,元组,interface不行。

  • type 可以使用 typeof 获取实例的类型进行赋值。

  • 多个相同的 interface 声明可以自动合并,多个相同的 type 声明会抛出错误。

  • interface 可以 extends 进行类型拓展,type 可以通过交叉类型进行类型拓展。

unknown, any的区别

unknown类型和any类型类似。与any类型不同的是。unknown类型可以接受任意类型赋值,但是unknown类型赋值给其他类型前,必须被断言

如何在 window 扩展类型

declare global {
  interface Window {
    myCustomFn: () => void;
  }
}

参考资源

1.2W字 | 了不起的 TypeScript 入门教程

一篇够用的TypeScript总结

最全的TypeScript学习指南

深入理解 TypeScript

Typescript 官方文档

TypeScript 中文文档