TypeScript

TypeScript 教程

1. TypeScript 概述

1.1 TypeScript 是什么

TypeScript 是一门基于 JavaScript 的编程语言,它是具有类型系统的 JavaScript,是一种解决 JavaScript 缺点的编程语言。

// 所有的 JavaScript 代码都是合法的 TypeScript 代码。
var a = 10;
function fn () {}
if (true) {}
for (var i = 0; i < 10; i++) {}
// TypeScript 是静态类型编程语言,即编译期间进行类型检查,变量、参数、返回值等都必须有固定的类型。
let x: number = 10;
x = 20;  // ✅
x = "a"; // ❎

在这里插入图片描述


TypeScript 不能在浏览器环境或者 Node 环境直接运行,它在执行前需要先被编译为 JavaScript。

// TypeScript
let isLogin: boolean = false
// Javascript
let isLogin = false;

TypeScript 提供的类型系统只应用在开发阶段,只有在开发阶段开发者才需要借助它编写出更加健壮的程序。


TypeScript 由微软发布,第一个正式版的发布时间为是2013年6月19日。

1.2 类型系统带来的好处

通过静态类型检查可以让开发者在编译时就能发现错误而不是在代码运行时,而且静态类型系统还大大增强了代码的可读性以及可维护性。

类型系统为编辑器带来了更加精准的代码提示,以此来提升开发人员的编程体验。

app.get("/", function (req, res) {
  res.
    send
    sendDate
    sendfile
    sendFile
    sendStatus
});

在声明变量时明确告知编译器它的类型,编译器就知道该变量可以调用哪些属性和方法,当开发者调用了错误的属性或方法时,编译器会及时给出提示。

var name: string = "张三";
name.toFixed(2); // 属性"toFixed"在类型"string"上不存在

在声明函数时明确告知编译器参数的类型,当开发者调用该函数时如果传递的参数的类型不正确,编译器会及时给出提示。

function sum(x: number, y: number) {}
sum(10, "a"); // 类型"string"的参数不能赋给类型"number"的参数。

在声明函数时明确告知编译器返回值的类型,当开发者返回的值的类型错误时,编译器会及时给出提示。

function sayHello(): string {
  return 100; // 不能将类型"number"分配给类型"string"。
}

在声明对象时告知编译器该对象下有哪些属性,当开发者访问了对象中不存在的属性时,编译器会及时给出提示。

const person = { name: "张三" };
person.age; // 类型"{name: string}"上不存在属性"age"。

类型系统使代码变得可预测,能够让开发者更早的发现代码存在的问题和潜在问题。

for (var i = 0; i < 5; i++) { }
console.log(i); // 编译器会捕获到错误: cannot find name "i"

当重复声明同名变量时,编译器会立即给出提示。

let colors = ["red", "green", "blue"];
// 无法重新声明块范围变量"colors"。
let colors = 10;

2. TypeScript 初体验

2.1 第一个 TypeScript 应用

需求:向服务端发送请求获取 ID 为 1 的任务对象。

目标:将 TypeScript 编译为 JavaScript。


  • 安装 TypeScript 编译器,用于将 TypeScript 代码编译为 JavaScript 代码
  • 安装 axios 库,用于发送请求
  • 新建 index.ts 文件,用于编写代码
  • 将 TypeScript 代码编译为 JavaScript 代码并执行

第一步:安装 TypeScript 编译器,用于将 TypeScript 代码编译为 JavaScript 代码

# 全局安装 typescript 编译器
npm install -g typescript
# 通过查看 typescript 编译器版本验证编译器是否安装成功
tsc -version

第二步:安装 axios 库,用于发送请求。

# 安装 axios 用于发送请求
npm install axios@0.27.2

第三步:新建 index.ts 文件用于编写代码,TypeScript 程序文件的后缀名为 .ts

import axios from "axios";

axios.get("https://jsonplaceholder.typicode.com/todos/1").then((response) => {
  console.log(response.data);
});

第四步:将 TypeScript 代码编译为 JavaScript 代码并执行。

# 编译 index.ts 文件, 编译后在同级目录下会多出 index.js 文件, 该文件存储的就是编译后的 JavaScript 代码
tsc index.ts
# 执行 JavaScript 代码
node index.js

2.2 优化工作流

目标:监控 TypeScript 文件的变化,实现自动编译、自动执行代码


  • 安装 nodemon、ts-node

  • 创建应用启动脚本

  • 通过应用启动脚本启动应用


# nodemon: 监听文件的变化, 当 TypeScript 文件内容发生变动后调用 ts-node
# ts-node: 将 TypeScript 编译为 JavaScript 并执行
npm install -g nodemon ts-node
// package.json
"scripts": {
  "start": "nodemon index.ts"
},
npm start

tsc 与 ts-node 的主要区别在于 tsc 根据 tsconfig 编译所有文件,ts-node 会从入口文件开始,并根据模块关系逐步转译文件。

2.3 体验类型带来的好处

需求:将任务ID、任务名称、任务是否完成分别输出到控制台中。

import axios from "axios";

axios.get("https://jsonplaceholder.typicode.com/todos/1").then((response) => {
  const todo = response.data;
  const id = todo.ID;
  const title = todo.Title;
  const finished = todo.finished;
  console.log(`
    任务的ID是: ${id},
    任务的名称是: ${title},
    任务是否完成: ${finished}
  `);
});

以上代码执行后,输出的结果都是 undefined,发生了什么?

任务的ID是: undefined,
任务的名称是: undefined,
任务是否完成: undefined

通过查看得知,任务 ID 对应的属性名称是 id,任务名称对应的属性名称是 title,任务是否完成对应的属性名称是 completed,原来是属性名称写错了。

目前的问题是在书写代码的过程中并没有任何的错误提示,只有代码运行以后开发者才能够知道代码中存在错误,这个问题应该怎么解决呢?

显式告知 TypeScript 编译器 response.data 中存储的数据的类型,编译器会实时检测你写的代码是否符合类型上的要求。

以下代码展示的是通过 TypeScript 约束对象中可以存在的属性,当访问了不存在的属性时编译器会实时进行提示。

import axios from "axios";

// interface 意为接口, 可以约束对象中可以有哪些属性, 约束对象中属性的类型 
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

axios.get("https://jsonplaceholder.typicode.com/todos/1").then((response) => {
  const todo = response.data as Todo;
  const id = todo.ID; // 属性"ID"在类型"Todo"上不存在。你是否指的是"id"?
  const title = todo.Title; // 属性"Title"在类型“Todo"上不存在。你是否指的是"title"?
  const finished = todo.finished; // 属性"finished"在类型"Todo"上不存在。
  console.log(`
    任务的ID是: ${id}, 
    任务的名称是: ${title}, 
    任务是否结束: ${finished}
  `);
});

以下代码展示的是通过 TypeScript 约束函数参数的类型,调用函数时如果传入的参数类型错误,编译器会实时进行提示。

import axios from "axios";

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

axios.get("https://jsonplaceholder.typicode.com/todos/1").then((response) => {
  const todo = response.data as Todo;
  const id = todo.id;
  const title = todo.title;
  const completed = todo.completed;
  logTodo(title, id, completed); // 类型"string"的参数不能赋给类型"number"的参数。
});

function logTodo(id: number, title: string, completed: boolean) {
  console.log(`
  任务的ID是: ${id},
  任务的名称是: ${title},
  任务是否结束: ${completed}
`);
}

3. TypeScript 基础类型

3.1 概述

以下表格中列出的所有类型在 TypeScript 中都是支持的。

JavaScriptTypeScript
numberany
stringunknow
booleannever
nullenum
undefinedtuple
object
array

3.2 基本数据类型

在 TypeScript 中,开发者可以通过类型注释对变量的类型进行标注。

// 数值类型
// :number 类型注释
let apples: number = 5;
// 字符串类型
let speed: string = "fast";
// 布尔值布尔
let hasName: boolean = true;
// TS2322: Type 'string' is not assignable to type 'number'
// 类型 'string' 不能分配给类型 'number'
apples = "5";    // ❎

// TS2322: Type 'number' is not assignable to type 'string'.
// 类型 'number' 不能分配给类型 'string'
speed = 120;     // ❎

// TS2322: Type 'string' is not assignable to type 'boolean'.
// 类型 'string' 不能分类给类型 'boolean'
hasName = "yes"; // ❎

3.3 any 类型

// any 类型
// 变量中可以存储任何数据类型的值
let anything: any = "Hello TypeScript";
anything = true;           // ✅
anything = 3.14;           // ✅
anything = function () {}; // ✅
anything = null;           // ✅

3.4 unknown 类型

unknow 是严格的 any 类型,在对 unknown 类型的变量执行操作之前必须先确定它的类型。

let anything: unknown = "Hello TypeScript";
anything = true;            // ✅
anything = 3.14;            // ✅
anything = function () {};  // ✅

// TS2571: Object is of type 'unknown'. ❎
// console.log(anything.length);

if (typeof anything === "number") {
  anything.toFixed();
} else if (typeof anything === "string") {
  anything.toUpperCase();
} else if (typeof anything === "function") {
  anything();
}

https://static.kancloud.cn/cyyspring/tyscript/2684794

3.5 数组 Array

// 在没有为数组变量标注类型时, 变量的初始值又是一个空数组
// 此时该数组中可以存储任何类型的值
// 虽然写法正确, 但丢失了 TypeScript 类型检查功能
// let colors = never[];
let colors = [];
// 字符串数组
let colors: string[] = ["red", "green", "blue"];
// 数值数组
let numbers: number[] = [100, 200, 300];
// 布尔数组
let bools: boolean[] = [true, true, false];
// 二维数组
const carMakers: string[][] = [["宝马", "比亚迪"]];
// let colors: string[]
// item: string
colors.forEach((item) => {});
// a: string
let a = colors[0];

3.6 元组 Tuples

元组可以按照顺序约束数组中每个下标对应的数据的类型。

[string, number, boolean] => [“a”, 100, false]

观察下列代码中存在的问题。

在 employee 数组中我们约定下标为0的位置存储员工姓名,下标为1的位置存储员工的年龄。

let employee = ["张三", 20];
employee[0] = 30;
employee[1] = "李四";

以上代码中存在的问题是 employee 数组中的元素没有被类型系统约束,导致在修改元素时没有任何错误提示。

元组是 TypeScript 引入的一种新数据类型,它像数组一样工作但是有一些额外的限制:元组中元素个数是固定,元组中元素类型已知。

元组用来存储一些以特定顺序出现且相关联的数据,通过元组可以约束元素个数及元素类型。

const employee: [string, number] = ["张三", 20];
// 不能将类型"number"分配给类型"string"
employee[0] = 30;
// 不能将类型"string"分配给类型"number"
employee[1] = "李四";
// 使用元组存储 RGB 颜色值及透明度
var bgColor: [number, number, number, number] = [0, 255, 255, 0.5];
// 创建元组数组(二维数组)
var employees: [number, string][] = [
  [1, "Steve"],
  [2, "Tom"],
];

3.7 枚举 Enum

枚举用于存储一组密切相关且有限的值,可以提升代码的可读性,可以限定值的范围,比如比赛结果,考试成绩,颜色种类,性别等等。

enum Sizes {
  Small,  // Small = 0
  Medium, // Medium = 1
  Large,  // Large = 2
}
console.log(Sizes.Small); // 0
enum Sizes {
  Small = 1,  // Small = 1
  Medium,     // Medium = 2
  Large,      // Large = 3
}
console.log(Sizes.Small); // 1
// 如果枚举值为非数值, 则每个属性都必须进行初始化
enum Sizes {
  Small = "s",
  Medium = "m",
  Large = "l",
}
console.log(Sizes.Small); // s
// 枚举被编译为了 JavaScript 中的对象
var Sizes;
(function (Sizes) {
    Sizes["Small"] = "s";
    Sizes["Medium"] = "m";
    Sizes["Large"] = "l";
})(Sizes || (Sizes = {}));
// 在声明枚举类型时, 如果使用 const 关键字, TypeScript 编译器将输出更加简洁的代码
const enum Sizes {
  Small = "s",
  Medium = "m",
  Large = "l",
}

let selected = Sizes.Large;
console.log(selected);
// 这是以上代码的编译结果
var selected = "l" /* Large */;
console.log(selected);
// 枚举使用示例
enum Sizes {
  Small = "s",
  Medium = "m",
  Large = "l",
}

let selected: Sizes = Sizes.Small;

function updateSize(size: Sizes) {
  selected = size;
}

updateSize(Sizes.Large);
场景使用不使用
消息的阅读状态YES
从1750年到现在的年份列表NO
菜单中饮料的种类YES
文章列表的所有标题NO
服务器端的电影分类NO
颜色选择器中的颜色YES

3.8 函数 Function

通过类型注释可以标注函数将要接收的参数的类型,函数将要返回的值的类型。

function add(a: number, b: number): number {
  return a + b;
}
const add = (a: number, b: number): number => {
  return a + b;
};
// TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
add(10, "20");
// let logNumber: Function;
let logNumber: (n: number) => number;
logNumber = (m) => {
  return m;
};

如果函数没有返回值,可以使用 void 标注。

function log(): void {}

TypeScript 编译器会检查实参的类型及参数数量是否正确。

function sum(a: number, b: number): number {
  return a + b;
}

// TS2554: Expected 2 arguments, but got 3.
sum(10, 20, 30);

TypeScript 编译器会检测函数的返回值类型是否正确。

// TS2366: Function lacks ending return statement and return type does not include 'undefined'.
function sum(a: number): number {
  if (a > 10) return a + 20;
}

TypeScript 编译器会检测函数内部没有使用的变量。

function log() {
  // 未使用的 局部变量 x 
  // 移除未使用的 局部变量 'x' 
  let x;
}

通过 ? 的方式设置可选参数, 它的类型是要么是它原本的类型要么是 undefined。

// c?: number | undefined
function sum(a: number, b: number, c?: number): number {
  if (typeof c !== "undefined") {
    return a + b + c;
  }
  return a + b;
}

sum(10, 20);

通过参数默认值的方式设置可选参数,它的类型就是原本的类型。

// c?: number
function sum(a: number, b: number, c: number = 0): number {
  return a + b + c;
}

sum(10, 20);

在定义函数时,如果形参被定义为解构语法,则使用下面的方式为函数形参设置类型。

const logWeather = ({ date, weather }: { date: Date; weather: string }) => {
  console.log(date, weather);
};

const today = {
  date: new Date(),
  weather: "sunny"
};

logWeather(today);
const profile = {
  // 年龄
  age: 20,
  // 坐标
  coords: {
    lat: 0,
    lng: 15,
  },
};

const { age }: { age: number } = profile;
const { coords }: { coords: { lat: number, lng: number } } = profile; // {lat: 0, lng: 15}
const { coords: { lat, lng } }: { coords: { lat: number, lng: number } } = profile;

// const { age }: { age: number } = profile;
// const {
//  coords: { lat, lng },
// }: { coords: { lat: number; lng: number } } = profile;

3.9 对象 Object

// 对象字面量
let point: { x: number; y: number } = { x: 100, y: 200 };
let employee: { readonly id: number } = { id: 1 };

// TS2540: Cannot assign to 'id' because it is a read-only property.
employee.id = 2;
// TS2741: Property 'name' is missing in type '{}' but required in type '{ name: string; }'.
let person: { name: string } = {};
let people = {};
// TS2339: Property 'name' does not exist on type '{}'.
// people.name = "张三";
// age?: number | undefined
let student: { age?: number } = {};
student.age = 20;
// 内置对象 类本身可以作为类实例的类型
let date: Date = new Date();

// 自定义类
// 类可以作为类实例的类型
class Car {}
let car: Car = new Car();

3.10 never 类型

never 表示永远不会发生的类型,即永远不能有值。比如用于抛出错误的函数,用于执行无限循环的函数,它们的返回值就是 never,再比如 never[],表示数组中不会有值。

const throwError = (message: string): never => {
  throw new Error(message);
};
// const throwError: (message: string) => void
const throwError = (message: string) => {
  if (!message) throw new Error("error");
};

注意:如果一个函数永远都不会有返回值,说明函数调用位置后面的代码永远都不会被执行。

3.11 可空类型

在 TypeScript 中,undefined 和 null 这两个值本身也可以作为类型使用。

// undefined
let nothing: undefined = undefined;
// null
let nothingMuch: null = null;
function greet(name: string | null | undefined) {
  if (typeof name === "string") {
    console.log(name.toLowerCase());
  } else {
    console.log("something went wrong");
  }
}

greet(undefined);

3.12 类型推断

TypeScript 编译器能根据一些简单的规则推断变量的类型。

在没有标注变量类型的情况下,编译器将变量初始值的类型作为该变量的类型。

// let apples: number
let apples = 5;
// let speed: string
let speed = "fast";
// let hasName: boolean
let hasName = true;
// let colors: string[]
let colors = ["red", "green", "blue"];
// let numbers: number[]
let numbers = [100, 200, 300];
// let bools: boolean[]
let bools = [true, true, false];
// let anything: never[]
let anything = [];

let ary: (string | number)[] = [1, 'a', 'b', 2, 'c', 'd', 3, 4];
// let point: {x: number, y: number}
let point = { x: 100, y: 200 };

TypeScript 编译器会试图推断函数返回值的类型。

// const add: (a: number, b: number) => number
const add = (a: number, b: number) => {
  return a + b;
};
// const add: (a: number, b: number) => void
const add = (a: number, b: number) => {};
// const add: (a: number, b: string) => string
const add = (a: number, b: string) => {
  return a + b;
};
// const add: () => string
const add = () => {
  return "a";
};
// const find: (name: string) => string | boolean
const find = (name: string) => {
  if (name) {
    return name;
  } else {
    return false;
  }
};

注意:使用函数返回值类型推断时,在编写函数内部代码时就失去了函数返回值类型检测功能,所以函数返回值的类型推荐明确指定。

在 TypeScript 编译器可以推断出变量类型的情况下,开发者不需要编写类型注释,也就是说只有在 TypeScript 不能正确推断变量类型的情况下开发者才需要编写类型注释,那么在哪些情况下 TypeScript 编译器不能正确推断出变量的类型呢?

① 如果变量声明后没有被立即初始化,TypeScript 编译器不能正确的推断出它的类型。

// 此时 TypeScript 编译器认为它是 Any 类型,即在该变量中可以存储任意类型的数据。
// 该变量失去了 TypeScript 中的类型检查功能
// let anything: any;
let anything;
anything = 12;
anything = "hello";
anything = true;
// 需求: 遍历 colors 数组, 从中查找绿色, 如果找到将 foundColor 变量的值设置为 true
let colors = ["red", "green", "blue"];
// let foundColor: any
// 解决办法是要么设置初始值, 要么显式指定变量类型
let foundColor;

for (let i = 0; i < colors.length; i++) {
  if (colors[i] === "green") {
    // 由于在声明 foundColor 时没有指定类型
    // 所以此处除了布尔类型以外的其他类型的值也可以设置
    foundColor = true;
  }
}

② 当调用的函数返回值为 Any 类型时,我们应该使用类型注释显式声明它的类型。

// let json: string;
let json = '{"name": "张三"}';
// let person: any => let person: {name: string}
let person = JSON.parse(json);

③ 当变量可能有多个类型的值时。

// 需求: 在数组中查找大于 0 的数值, 如果找到将该值赋值给变量 target, 如果没有找到, 将 false 赋值给变量 target
let numbers = [-10, -1, 20];
// target => boolean | number
let target = false;

for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] > 0) {
    // 不能将类型"number"分配给类型"boolean"
    target = numbers[i];
  }
}
let target: boolean | number = false;

④ 函数的参数必须标注类型,TypeScript 并不能推断函数参数的类型。

// TS7006: Parameter 'a' implicitly has an 'any' type
function sum(a, b) {}

4. TypeScript 高级类型

4.1 声明类型

通过 type 关键字可以声明类型,声明的类型可以是基本数据类型也可以是复杂数据类型。

注意:通过 interface 只能声明复杂数据类型。

// 也可以将此处的声明理解为 为 string 类型起了个别名
type Name = string | number | boolean;
let nameValue: Name = "张三";
type Point = {
  x: number;
  y: number;
  add: (x: number, y: number) => number;
};

let point: Point = { x: 100, y: 200, add: (x: number, y: number) => x + y };
type Person = { name: string; age: number };

let p1: Person = { name: "张三", age: 20 };
let p2: Person = { name: "李四", age: 50 };
let p3: Person = { name: "王五", age: 60 };
let p4: Person = { name: "赵六", age: 35 };

4.2 联合类型

联合类型是指将多个类型进行联合的使用,联合的结果就是新的类型。

为变量设置联合类型就意味着该变量可以是联合类型中的任意类型。

// 联合类型: 为一个变量设置多个类型
let a: string | number | boolean = true;
// arg 参数既可以是字符串类型也可以是数值类型
function fn(arg: string | number) {}
fn("a");
fn(10);

在使用联合类型的变量时,编辑器默认只能列出联合类型中所有类型的同名属性,因为 TypeScript 编译器并不能确定它的具体类型是什么。

在使用联合类型的变量时通常需要先缩小变量的类型范围,这样编辑器才能给出更加精准的代码提示。

通过类型判断可以缩小变量的类型范围,通过类型判断缩小变量范围再进行变量操作的行为也被叫做类型守卫。

观察下列代码中存在的问题。

// 披萨的尺寸
let pizzaSize: 'large' | 'medium' | 'small' = 'medium'

function selectPizzaSize (size: 'large' | 'medium'| 'small') {
  pizzaSize = size
}

selectPizzaSize('small')

以上代码同一个类型被声明了多次导致重复代码过多,可以使用 type 声明类型复用类型。

type Size = 'large' | 'medium' | 'small'; 

let pizzaSize: Size = 'medium'

function selectPizzaSize (size: Size) {
  pizzaSize = size
}

selectPizzaSize('large')

4.3 交叉类型

交叉类型是指将多个类型叠加合并组成新的类型,新类型中包含了被合并类型的所有属性。

type Draggable = {
  drag: () => void;
};

type Resizable = {
  resize: () => void;
};

type UIWidget = Draggable & Resizable;

let textBox: UIWidget = {
  drag: () => {},
  resize: () => {},
};

4.4 字面量类型

字面量类型是指将字面量值的类型作为变量的类型,将字面量值作为变量的取值范围。

将字面值作为变量的取值范围可以有效避免书写错误。

type Quantity = 50 | 100;

// TS2322: Type '54' is not assignable to type 'Quantity'.
let quantity: Quantity = 54;
type Metric = "cm" | "inch";

// TS2322: Type '"abc"' is not assignable to type 'Metric'.
let metric: Metric = "abc";

4.5 可选链操作符

目标:掌握可选链操作符的使用方式


  • 分析一段 JavaScript 代码,为讲解可选链操作符做铺垫
  • 可选链操作符和对象一起使用
  • 可选链操作符和数组一起使用
  • 可选链操作符和函数一起使用

① 先来看一段 JavaScript 中的代码,分析代码中的潜在问题。

// person 对象中的 name 属性是可选的, 即 person 对象中可能有 name 属性也可能没有 name 属性
// 如果 name 属性存在, 它是字符串类型
const person = { name: "John" };
console.log(person.name.toUpperCase()); // JOHN
const person = {};
console.log(person.name.toUpperCase());
// Uncaught TypeError: Cannot read properties of undefined (reading 'toUpperCase')
const person = {};
if (person.name) console.log(person.name.toUpperCase());

可选链操作符( ?. )允许开发者安全的链式访问一个对象上可能为 nullundefined 的属性。

② 可选链操作符和对象一起使用

interface Person {
  name?: string;
}

const person: Person = { name: "张三" };
person.name?.toLocaleUpperCase();

③ 可选链操作符和数组一起使用

interface Person {
  skills: string[] | null;
}

const person: Person = {
  skills: ["编程", "开锁", "飞檐走壁"],
};

person.skills?.forEach((item) => console.log(item));

③ 可选链操作符和函数一起使用

interface Person {
  sayHello?: () => void;
}

const person: Person = {
  sayHello: () => {
    alert("Hello");
  },
};

person.sayHello?.();

4.6 空值合并运算符

空值合并运算符 (??) 是一个逻辑运算符,当左侧的运算数为 null 或者 undefined 时,返回其右侧运算数,否则返回左侧运算数。

let speed: number | null = null;

let ride = {
  // speed: speed !== null ? speed : 30,
  speed: speed ?? 30,
};

4.7 类型断言

通过类型断言可以覆盖 TypeScript 编译器的推断,当开发者比 TypeScript 更加清楚它的类型时使用。

// const phone: HTMLElement | null
const phone = document.getElementById("phone");

console.log((<HTMLInputElement>phone).value);
console.log((phone as HTMLInputElement).value);

!非空断言

4.8 函数重载

函数重载是指多个同名函数具有不同的调用签名,通过不同的调用签名决定到底执行哪一个具体的函数,执行的函数不同做的事情可以不同。

// Java 中的函数重载
public class Sum {
  public int sum(int x, int y) {
    return (x + y);
  }
  public int sum(int x, int y, int z) {
    return (x + y + z);
  }
  public double sum(double x, double y) {
    return (x + y);
  }
  public static void main(String args[]) {
    Sum s = new Sum();
    System.out.println(s.sum(10, 20));
    System.out.println(s.sum(10, 20, 30));
    System.out.println(s.sum(10.5, 20.5));
  }
}

JavaScript 没有函数重载特性,虽然没有但是我们可以通过代码模拟实现。

需求:定义 sum 函数,接收两个参数 n 和 m,当 n 和 m 都是数值时进行数值相加,当 n 和 m 都是字符串时进行字符串连接。

// 在 JavaScript 中通过代码模拟函数重载功能
function sum(n, m) {
  if (typeof n === "number" && typeof m === "number") {
    // 数值相加
    return n + m;
  } else if (typeof n === "string" && typeof m === "string") {
    // 字符串连接
    return n + "_" + m;
  }
}

sum(10, 20); // 30
sum("hello", "world"); // hello_world
// 在 TypeScript 中实现函数重载
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any) {
  if (typeof a === "string" && typeof b === "string") {
    return a + b;
  } else if (typeof a === "number" && typeof b === "number") {
    return a + "_" + b;
  }
}

const result1 = add(10, 20);
const result2 = add("a", "b");

需求:调用 getMessage 方法获取消息,如果传递的参数是数值类型,表示根据消息 id 获取消息对象,如果传递的参数是字符串类型,则表示根据消息种类获取消息列表。

// 消息种类的类型
type MessageType = "string" | "image" | "audio";
// 消息对象的类型
type Message = {
  id: number;
  type: MessageType;
  content: string;
};
// 消息数组
const data: Message[] = [
  { id: 1, type: "string", content: "hello-1" },
  { id: 2, type: "image", content: "hello-2" },
  { id: 3, type: "audio", content: "hello-3" },
];
function getMessage(id: number): Message | undefined;
function getMessage(type: MessageType): Message[];
function getMessage(query: any): any {
  if (typeof query === "number") {
    return data.find(message => message.id === query);
  } else {
    return data.filter(message => message.type === query);
  }
}
// const r1: Message | undefined
const r1 = getMessage(1);
// const r2: Message[]
const r2 = getMessage("image");

4.9 索引签名

需求: 定义一个电影院放映厅的座位对象并为每一个座位分配人员,对象属性为座位编号,属性值为看电影的人的名字。

const seats: { A1: string; A2: string } = { A1: "", A2: "" };

在以上代码中存在的问题是,在创建座位对象时我们并不知道每个座位编号对应的人是谁,而且座位又有上百个,如果在定义对象时一定要将所有座位编号定义出来的话,那将一次性定义上百个属性,而每一个属性的值都是空字符串,这样的代码写出来是没有意义的。座位是伴随着时间动态卖出去的, 所以为座位对象分配人员这件事也应该是动态进行的,也就是说我们应该动态为座位对象添加属性分配人员。


在 JavaScript 中,为对象动态添加属性是被允许的,但是在 TypeScript 中是不行的,因为在 TypeScript 中对象要有严格的类型限制。

let person = {};
// TS2339: Property 'name' does not exist on type '{}'.
person.name = "张三";

在 TypeScript 中动态为对象添加属性要使用索引签名,它可以限制对象中属性的类型和属性值的类型,只要满足条件的键值对都可以动态添加到对象中。

// 属性类型的定义使用中括号包裹, 索引签名中定义的属性名称并不是真正的属性名称, 它是用来占位的, 但要求名称要具有含义
// [seatNumber: string] 属性的类型
// : string 属性值的类型
const seats: { [seatNumber: string]: string } = {};

seats.A1 = "张三";
seats.A2 = "李四";

4.10 类型谓词

类型谓词用于缩小函数参数的类型范围。

如果一个函数的返回值为真,可以指定函数的某一个参数的类型。

// 如果 fn 函数的返回值为真, arg 参数的类型就是 string
// parameterName is Type
function fn(arg): arg is string {
  return true;
}
// 鱼
type Fish = {
  swim: () => void;
};
// 鸟
type Bird = {
  fly: () => void;
};

// 获取宠物 可能得到鱼 也可能得到鸟
function getPet(): Fish | Bird {
  // 鱼对象
  const fish = {
    swim() {
      console.log("swim");
    },
  };
  // 鸟对象
  const bird = {
    fly() {
      console.log("fly");
    },
  };
  return Math.round(Math.random() * 10) > 5 ? fish : bird;
}

// 判断 pet(宠物) 参数是否为鱼类型
function isFish(pet: Fish | Bird): pet is Fish {
  return (<Fish>pet).swim !== undefined;
}

// 获取宠物
let pet = getPet();

// 判断宠物类型
if (isFish(pet)) {
  //  如果是鱼调用游泳方法
  pet.swim();
} else {
  // 否则调用鸟的飞翔方法
  pet.fly();
}
// 返回参数 value 的数据类型
function getType(value: unknown) {
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

// isString 函数的返回值为布尔类型
// 如果 isString 函数的返回值为 true, TypeScript 编译器就认定 s 是字符串类型
function isString(value: unknown): value is String {
  return getType(value) === "string";
}

function toUpperCase(value: unknown) {
  if (isString(value)) {
    value.toUpperCase();
  }
}

5. 面向对象编程

5.1 概述

面向对象和面向过程都属于编写程序的指导思想,指导开发者按照什么样的套路编写代码。

面向过程是侧重于过程的编程,即按照功能代码的执行顺序编写代码。在编写程序时要明确知道每一步要做什么,比如将大象装冰箱一共分为三步,那么按照步骤编写代码即可。

面向对象是侧重于对象的编程,开发者站在上帝视角,要想什么就创建什么对象,然后赋予对象做事情的能力,要做的事情让对象自己完成。比如将大象装冰箱,我们要创建大象对象,创建冰箱对象,赐予大象进冰箱的能力,然后让大象自己进冰箱。

面向过程的优点是简单,思考简单,编写简单。

面向过程的缺点是代码量越大维护成本越高。当程序出问题后,需要从头到尾将步骤过一遍,在此过程中找问题,修复问题,如果代码中步骤非常多的话,是非常耗时间和精力的。

面向对象的优点的是扩展性强维护成本低,当程序出问题后定位对象即可,哪个对象出问题就找哪个对象修复哪个对象就可以了。

面向对象的缺点是新人上手难度高。

5.2 类与对象

在面向对象中通过对象来表示具体的事物,对象才可以干具体的事情,那么我们如何创建对象呢?比如创建一个表示车的对象。

现实世界中第一步是画车辆图纸,在图纸中定义车的特征和车的能力,比如车是什么颜色,有多少座位,车可以驾驶。第二步是根据图纸造真实车辆。

面向对象中使用类表示图纸,定义类就是画图纸,类中包含两部分,属性和方法,属性就是特征,方法就是能力。通过类可以创造对象。对象能干什么完全取决于你的类是如何定义的。

class Vehicle {
  // 特性: 颜色
  color = "白色";
  // 特性: 最高速度
  maxSpeed = 220;
  // 能力: 驾驶
  drive() {
    console.log("run");
  }
  // 能力: 鸣笛
  honk = () => {
    console.log("didi");
  }
}
const vehicle = new Vehicle();
console.log(vehicle);
console.log(vehicle.color);
console.log(vehicle.maxSpeed);
vehicle.drive();
vehicle.honk();

通过同一个类可以创建多个相同的实物对象,所以属性就是这一类事物拥有的共同特性,方法就是这一类事物拥有的共同的能力。

const v1 = new Vehicle();
const v2 = new Vehicle();
const v3 = new Vehicle();

通过类创建出来的对象也被称之为实例对象。

function Vehicle () {
  this.color = '白色';
  this.maxSpeed = 220;
  this.honk = function () {
    console.log('didi')
  }
}

Vehicle.prototype.drive = function () {
  console.log('run')
}

5.3 构造函数

构造函数用来为对象属性赋初始值或者执行初始化操作,构造函数的名字是固定的,即 constructor。

class Vehicle {
  constructor () {}
}

在使用 new 关键字创建对象时可以传递参数、该参数在类的内部通过构造函数接收,参数值一般就是对象中某个属性的初始值。

class Vehicle {
  constructor (color: string, maxSpeed: number) {}
}

const vehicle = new Vehicle("红色", 240);

在构造函数中 this 关键字指向的是通过类创建出来的对象,所以通过 this 可以找到对象的属性,然后就可以为属性赋值了。

class Vehicle {
  color: string;
  maxSpeed: number;

  constructor(color: string, maxSpeed: number) {
    this.color = color;
    this.maxSpeed = maxSpeed;
  }
}

const vehicle = new Vehicle("红色", 240);

类的构造函数是被自动调用的,在使用 new 关键字创建对象时被自动调用。

5.4 只读属性

只读属性是指属性一旦被赋值,该值就不能被修改。

在类中通过 readonly 修饰符将类属性设置为只读属性。

class Vehicle {
  readonly maxSpeed: number;
  constructor(maxSpeed: number) {
    this.maxSpeed = maxSpeed;
  }

  drive() {
    //  TS2540: Cannot assign to 'maxSpeed' because it is a read-only property.
    this.maxSpeed = 900;
  }
}

const vehicle = new Vehicle(240);
// TS2540: Cannot assign to 'maxSpeed' because it is a read-only property.
vehicle.maxSpeed = 300;

5.5 继承

在面向对象编程中,很多对象都会具有相同的特征和能力,比如猫对象和狗对象,它们都具有五官特征都具备吃饭和奔跑的能力,如果我们分别在猫类和狗类中定义五官特征和奔跑能力的话,那么程序中将出现大量的重复性代码。

要解决上述问题,我们可以对类别进行更高级别的抽象,比如我们可以再定义一个动物类,将动物具备的通用特性定义在动物类中,比如五官特征和奔跑吃饭的能力。那么猫类和狗类如何拥有这些特征和能力呢?

答案就是继承,通过继承可以让具体的猫类和狗类拥有这些特征和能力,这样程序中就不会出现重复性代码了,然后我们还可以在猫类和狗类中继续定义属于猫和狗的独有特征和能力。

class Animal {}

class Dog extends Animal {}
class Cat extends Animal {}

在以上代码中,Animal 被称之为父类,Dog 和 Cat 被称之为子类。

在程序中分别创建汽车分类,轿车分类、面包车分类,在汽车分类下描述所有汽车的通用特征,在细化分类下只描述该分类独有的特征。

// 汽车分类
class Vehicle {
  drive() {
    console.log("run");
  }
}

class Car extends Vehicle {
  brand = "BMW";
}

class Van extends Vehicle {
  brand = "五菱宏光";
}

let car = new Car();
car.drive();
car.brand;

let van = new Van();
van.drive();
van.brand;

在子类继承父类后,如果父类需要在初始化的时候传递参数,该参数由子类接收,在子类的构造函数中通过 super 调用父类将参数传递给父类。

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

class Student extends Person {
  studentId: number
  // 注意: 在子类继承父类后, 如果要在子类中使用构造函数, 即使父类不需要传递参数
  // 在构造函数中的第一件事情也是必须先调用 super 方法
  constructor(studentId: number, name: string) {
    super(name);
    this.studentId = studentId;
  }
}

const p1 = new Student(1, "张三");
console.log(p1.studentId);
console.log(p1.name);

子类在继承父类后,还可以通过重写父类方法对功能进行扩展。

class Person {
  walk() {
    console.log("person walk");
  }
}

class Student extends Person {
  // override 表示该方法是覆盖的父类方法, 仅作为标识提升代码的可阅读性, 不会对代码的实际执行产生任何副作用
  override walk() {
    console.log("student walk");
    // 通过 super 调用父类 walk 方法
    super.walk();
  }
}

const p1 = new Student();
p1.walk();

5.6 访问权限修饰符

通过访问权限修饰符可以指定类中的属性、方法能够在哪些范围内被访问。

修饰符作用
public被 public 关键字修饰的类属性和类方法可以在任何地方使用 (当前类、子类、实例对象)
private被 private 关键字修饰的类属性和类方法只能在当前类中使用
protected被 protected 关键字修饰的类属性和类方法可以在当前类和子类中使用

① public 公开的

类属性和类方法在不加任何权限修饰符的情况下,它就是可以在任何地方被访问的,也就是说 public 是默认值,是可以被省略的。

class Vehicle {
  constructor() {
    // 1. 在本类中的其他方法中使用
    this.drive();
  }
  public drive() {
    console.log("run");
  }
}

class Car extends Vehicle {
  drive() {
    // 2. 在子类中使用
    super.drive();
  }
}

const car = new Car();
// 3. 在实例对象中使用
car.drive();

② private 私有的

class Vehicle {
  constructor() {
    // 在本类中的其他方法中使用
    this.drive();
  }
  private drive() {
    console.log("run");
  }
}

class Car extends Vehicle {
  drive() {
    // 属性"drive"为私有属性,只能在类"Vehicle"中访问
    super.drive();
  }
}

const vehicle = new Vehicle();
// 属性"drive"为私有属性,只能在类"Vehicle"中访问
vehicle.drive();

③ protected 受保护的

class Vehicle {
  constructor() {
    // 1. 在本类中的其他方法中使用
    this.drive();
  }
  protected drive() {
    console.log("run");
  }
}

class Car extends Vehicle {
  drive() {
    // 2. 在子类中使用
    super.drive();
  }
}

const vehicle = new Vehicle();
// 属性"drive"受保护, 只能在类"Vehicle"及其子类中访问
vehicle.drive();

5.7 Getter 与 Setter

Getter 和 Setter 是对属性访问(获取和设置)的封装,获取属性值时走 Getter,修改属性值时走 Setter。

可以将属性访问器理解为对属性的保护。

在获取属性或修改属性值时,通过属性访问器可以监听到,监听到以后可以做一些额外的事情。

class Employee {
  private _salary: number;
  constructor(salary: number) {
    this._salary = salary;
  }

  get salary() {
    return this._salary;
  }

  set salary(salary: number) {
    this._salary = salary;
  }
}

const employee = new Employee(4000);
console.log(employee.salary);
employee.salary = 6000;
console.log(employee.salary);
# 通过选项的方式指定 target
tsc -t es2016 index.ts
# 通过配置文件的方式指定 target
tsc --init
# 若要走配置文件 直接执行 tsc 命令
tsc

5.8 参数属性

TypeScript 提供了特殊的语法将构造函数参数转换为具有相同名称的类属性。

通过在构造函数参数的前面加上权限修饰符 public、private、protected 或 readonly 创建。

class Params {
  x: number;
  y: number;
  z: number;
  constructor (x: number, y: number, z: number) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
}
class Params {
  constructor(
    public x: number,
    public y: number,
    public z: number
  ) {}

  log() {
    console.log(this.x);
    console.log(this.y);
    console.log(this.z);
  }
}

const p = new Params(10, 20, 30);
p.log();

5.9 静态成员

在类中被 static 关键字修饰的类属性和类方法被叫做静态成员,静态成员属于类,所以访问静态成员的方式是类名点上静态成员名称。

class Rich {
  static count: number = 0;
}
// 通过 类名 + 静态成员名称 访问静态成员
Rich.count;
// 以下写法错误
// 静态成员不属于类的实例对象, 所以不能通过类的实例对象访问静态成员
const r = new Rich();
// TS2576: Property 'count' does not exist on type 'Rich'. 
// Did you mean to access the static member 'Rich.count' instead?
r.count;
class Rich {
  static count: number = 0;
  getCount () {
    return Rich.count;
  }
}

对于该类的任何一个对象而言,静态属性就是公共的存储单元,该类的任何对象访问它时,取到的都是相同的值,该类的任何对象修改它时,也都是在对同一个内存单元做操作,所以静态属性主要用在各个对象都要共享的数据。

目标: 获取类的实例对象的数量。

 class Rich {
  private static _count: number = 0;

  constructor() {
    Rich._count++;
  }

  getCount() {
    return Rich._count;
  }
}

const r1 = new Rich();
const r2 = new Rich();

console.log(r1.getCount());
console.log(r2.getCount());

因为静态成员始终存在于内存,而非静态成员需要实例化才可以分配到内存,所以静态成员不能访问非静态成员,非静态成员可以访问类中的静态成员。

class Rich {
  private static _count: number = 0;
  // 静态成员中不能访问非静态成员
  // 静态方法中的 this 指向了类, 而不是类实例
  static fn() {
    // TS2339: Property 'getCount' does not exist on type 'typeof Rich'.
    // this.getCount();
  }
  // 非静态成员中可以访问静态成员
  getCount() {
    return Rich._count;
  }
}

5.10 抽象类

抽象类因继承而存在,通过抽象类可以约束子类必须实现哪些成员,如果抽象类不被继承,它将毫无意义。

在抽象类中可以只定义成员,具体的成员实现由子类完成且必须完成,所以抽象类不能被直接实例化。

abstract class Shape {
  abstract color: string;
  abstract render(): void;
}

class Circle extends Shape {
  constructor(public color: string) {
    super();
  }
  override render(): void {
    console.log("render");
  }
}

// TS2511: Cannot create an instance of an abstract class.
// new Shape();

const circle = new Circle("red");
console.log(circle.color);
circle.render();

6. 接口 Interface

接口用于声明类型,用于对复杂的数据结构进行类型描述,比如对象、函数、类。

6.1 接口概基本使用

声明接口需要使用 interface 关键字。

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

使用接口约束对象的类型。

const user: User = {
  name: "张三",
  age: 20,
};

使用接口约束函数的类型。

interface Sum {
  (n: number, m: number): number;
}

const sum: Sum = function (a, b) {
  return a + b;
};

使用接口约束类的成员。

interface Calendar {
  name: string;
  addEvent(): void;
  removeEvent(): void;
}

class GoogleCalendar implements Calendar {
  name: string = "test";
  addEvent(): void {}
  removeEvent(): void {}
}

6.2 宽松的接口检查

TypeScript 接口检查是宽松的,当变量满足了接口规范以后,即使变量中存在接口规范以外的属性也是可以的。

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

let user: User = {
  name: "张三",
  age: 20,
};

let someone = {
  name: "李四",
  age: 50,
  sex: "男",
};

user = someone;
interface Reportable {
  summary(): void;
}

function printSummary(item: Reportable): void {
  item.summary()
}

const person = {
  name: "张三",
  summary() {
  	console.log(`您好, 我的名字叫${this.name}`);
  },
};

printSummary(person);

对于宽松的接口检查政策字面量是个例外,也就是说对于字面量的接口类型检查是严格的,不能出现接口规范以外的其他属性。

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

// ✅
const user: User = { name: "张三", age: 20 };

// ❌
// 不能将类型"{ name: string; age: number; sex: string; }"分配给类型"User"
// 对象字面量只可以指定已知属性, "sex"不在类型"User"中
const another: User = { name: "李四", age: 40, sex: "男" };
interface Reportable {
  summary(): void;
}

function printSummary(item: Reportable): void {
  item.summary();
}

// ❌
// 类型"{ name: string; summary(): void; }"的参数不能赋给类型"Reportable"的参数。
// 对象字面量只可以指定已知属性, "name"不在类型"Reportable"中。ts(2345)
printSummary({
  name: "张三",
  summary() {
    console.log(`您好, 我的名字叫${this.name}`);
  },
});

那么如何绕过字面量严格类型检查模式呢?

// 使用类型断言
interface User {
  name: string;
  age: number;
}

const another: User = { name: "李四", age: 40, sex: "男" } as User;
// 使用索引签名
interface User {
  name: string;
  age: number;
  [key: string]: string | number;
}

const another: User = { name: "李四", age: 40, sex: "男" };

6.3 接口继承

接口具有继承特性即接口与接口之间可以存在继承关系,而且一个接口可以继承多个接口。

// 接口继承示例
interface Sizes {
  sizes: string[];
  getAvailableSizes(): string[];
}

interface Shape {
  color: string;
}

interface Pizza extends Sizes, Shape {
  name: string;
}

let pizza: Pizza = {
  name: "张三",
  color: "skyblue",
  sizes: ["large", "small"],
  getAvailableSizes() {
    return this.sizes;
  },
};

在继承了接口以后可以对被继承接口中的属性进行重写,但是重写的类型一定要在原有类型的范围以内。

interface User {
  // name: string | number | boolean;
  name: any;
  age: number;
}

interface MyUser extends User {
  name: boolean;
}

6.4 接口合并

接口具有声明合并特性,即多个相同名称的接口会自动合并。

interface Box {
  height: number;
  width: number;
}

interface Box {
  scale: number;
}

let box: Box = { height: 5, width: 6, scale: 10 };

6.5 接口函数重载

interface GetMessage {
  (id: number): Message | undefined;
  (type: MessageType): Message[];
}

const getMessage: GetMessage = (query: any): any => {
  if (typeof query === "number") {
    return data.find((message) => message.id === query);
  } else {
    return data.filter((message) => message.type === query);
  }
};

7. 泛型 Generic

7.1 概述

泛型是指将类型作为参数进行传递,通过参数传递类型解决代码复用问题 ( 代码中类型的复用问题,通过类型复用减少重复代码 )。

泛型传参和函数传参要解决的问题是一样的。

function a() {
  return 1 + 1;
}

function b() {
  return 2 + 2;
}

function c() {
  return 3 + 3;
}
function sum(n: number, m: number) {
  return n + m;
}
sum(1, 1);
sum(2, 2);
sum(3, 3);

7.2 泛型函数

目标:声明一个函数,接收一个参数,接收什么参数返回什么值。

// 失去了 ts 中的类型检查
function getValue(value: any): any {
  return value;
}
function getString(value: string): string {
  return value;
}

function getNumber(value: number): number {
  return value;
}
function getValue<T>(value: T): T {
  return value;
}

getValue<string>("hello");
getValue<number>(100);

// 泛型实参可以忽略 你传递的参数的类型就是要传递的泛型的类型
getValue("hello");

7.3 泛型类

需求:通过类创建对象,对象的 key 属性可以是字符串可以是数值,对象的 value 属性可以是字符串可以是数值。

{key: string, value: string}
{key: number, value: number}
class StringKeyValuePair {
  constructor(public key: string, public value: string) {}
}

class NumberKeyValuePair {
  constructor(public key: number, public value: number) {}
}
class KeyValuePair<K, V> {
  constructor(public key: K, public value: V) {}
}

需求:通过类创建对象,对象中有 collection 属性,属性值为数组,数组中可以存储字符串也可以存储数值,通过索引可以获取到collection数组中的值。

class ArrayOfNumbers {
  constructor(public collection: number[]) {}
  get(index: number): number {
    return this.collection[index];
  }
}

class ArrayOfStrings {
  constructor(public collection: string[]) {}
  get(index: number): string {
    return this.collection[index];
  }
}

在以上代码中,数值数组类和字符串数组类所做的事情是一样的,但由于创建的数据类型不同,所以写成了两个类,它们属于重复代码。

class ArrayOfAnything<T> {
  constructor(public collection: T[]) {}
  get(index: number): T {
    return this.collection[index];
  }
}
// constructor ArrayOfAnything<number>(collection: number[]): ArrayOfAnything<number>
new ArrayOfAnything<number>([1, 2, 3]);
// constructor ArrayOfAnything<string>(collection: string[]): ArrayOfAnything<string>
new ArrayOfAnything<string>(["a", "b", "c"]);

7.4 泛型接口

需求:创建 fetch 方法用于获取数据,当获取用户数据时,fetch 方法的返回值类型为用户,当获取产品数据时, fetch 方法的返回值类型为产品,不论是用户数据还是产品数据都要被包含在响应对象中。

interface MyUserResponse {
  data: User;
}

interface MyProductResponse {
  data: Product;
}
interface MyResponse<T> {
  data: T | null;
}

function fetch<T>(): MyResponse<T> {
  return { data: null };
}

interface User {
  username: string;
}

interface Product {
  title: string;
}

fetch<User>().data?.username;
fetch<Product>().data?.title;

7.5 泛型约束

泛型约束是指对泛型参数的范围进行约束,就是说虽然类型可以被当做参数传递,但是传递的类型不能是随意的想传什么就传什么,通过泛型约束可以限制能够传递的类型的范围。

// 限制类型 T 的范围, 就是说 T 的类型要么是字符串要么是数值 其他的是不可以的
class StringOrNumberArray<T extends string | number> {
  constructor(public collection: T[]) {}
  get(index: number): T {
    return this.collection[index];
  }
}

new StringOrNumberArray<string>(["a", "b"]);
new StringOrNumberArray<number>([100, 200]);
// 类型"boolean"不满足约束"string | number"
// new StringOrNumberArray<boolean>([true, false]);
function echo<T extends string | number>(value: T): T {
  return value;
}
echo<string>("Hello"); // ✅
echo<number>(100);     // ✅
echo<boolean>(true);   // ❎ 类型"boolean"不满足约束"string | number". ts(2344)
interface Person {
  name: string;
}

function echo<T extends Person>(value: T): T {
  return value;
}

echo<Person>({ name: "张三" }); // ✅
class Person {
  constructor(public name: string) {}
}

class Custom extends Person {}

function echo<T extends Person>(value: T): T {
  return value;
}

echo<Person>(new Person("张三"));
echo<Custom>(new Custom("李四"));
interface Printable {
  print(): void;
}

function echo<T extends Printable>(target: T) {
  target.print();
}

class Car {
  print() {}
}

class Hourse {
  print() {}
}

echo<Car>(new Car());
echo<Hourse>(new Hourse());

8. 类型操作符

8.1 keyof 操作符

keyof 是类型运算符,接收类型返回类型,返回的是接收类型的属性字面量联合类型。

interface Product {
  name: string;
  price: number;
}

// 属性字面量联合类型 name | price
// type ProductKeys = "name" | "price"
type ProductKeys = keyof Product;

const keys1: ProductKeys = "price";
const keys2: ProductKeys = "name";
// 不能将类型"a"分配给类型“keyof Product”。
// const keys3: ProductKeys = "a";
// 在 store 类中声明 find 方法, 该方法的作用是根据属性和属性值在 _objects 数组中查找对象
// [{name: "张三"}, {price:100}]
// store.find("name", "张三"); // {name: "张三"}

class Store<T> {
  protected _objects: T[] = [];

  add(obj: T) {
    this._objects.push(obj);
  }

  find(property: keyof T, value: unknown): T | undefined {
    return this._objects.find((obj) => obj[property] === value);
  }
}

interface Product {
  name: string;
  price: number;
}

const store = new Store<Product>();
store.add({ name: "Hello", price: 100 });
store.find("name", "Hello");
store.find("price", 100);
// TS2345: Argument of type '"a"' is not assignable to parameter of type 'keyof Product'.
store.find("a", 12);
// TS2345: Argument of type '"greet"' is not assignable to parameter of type 'keyof Product'.
store.find("greet", "hi");
// 多学一招: 获取类型中某一个属性值的类型
interface Product {
  name: string;
  price: number;
}
// type nameType = string
type nameType = Product["name"];
// type nameType = number
type priceType = Product["price"];

8.2 typeof 操作符

TypeScript 在语言层面又添加了一个新的 typeof 运算符,该运算符可以用来获取一个变量的类型。

const person = {
  name: "张三",
  age: 20,
};
type Person = typeof person;
function fn() {
  return false;
}

type r = ReturnType<typeof fn>;

10. 映射类型

10.1 概述

映射类型是指基于现有类型产生新的类型。

通过遍历语法拷贝原有类型,再在拷贝类型的基础上进行修改从而产生新的类型。

如何拷贝原有类型?拷贝原有类型主要拷贝的是两部分,原有类型中有哪些属性、属性的值是什么类型。

interface Product {
  name: string;
  price: number;
}

type NProduct = {
  // 1. 如何拷贝原有类型中的属性?
  // keyof Product => "name" | "price"
  // K in (name | price)
  // name
  // price
  
  // 2.如何获取属性(name, price)值原有的类型
  // 类型[属性名称] 得到的结果就是属性值的类型
  // Product[K] => Product[name] => string
  // Product[K] => Product[price] => number
  [k in keyof Product]: Product[k];
};

10.2 MyReadOnly

目标:基于 Product 类型,创建新类型,新类型中的属性都是只读的。

// 最终目标如下
interface Product {
  name: string;
  price: number;
}

type ReadOnlyProduct = {
  readonly name: string;
  readonly price: number;
};
type ReadOnlyProduct = {
 readonly [K in keyof Product]: Product[K];
};

let product: ReadOnlyProduct = {
  name: "test",
  price: 100,
};

// TS2540: Cannot assign to 'name' because it is a read-only property.
product.name = "hello";

目标:创建 MyReadOnly 类型,通过泛型参数接收旧的类型,返回新类型,新类型中的属性都是只读的。

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

let product: MyReadOnly<Product> = {
  name: "test",
  price: 100,
};

// TS2540: Cannot assign to 'name' because it is a read-only property.
product.name = "hello";

10.3 MyOptional 类型

目标:创建 MyOptional 类型,通过泛型参数接收旧的类型,返回新类型,新类型中的属性都是可选的。

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

10.4 MyNullable 类型

目标:创建 MyNullable 类型,通过泛型参数接收旧的类型,返回新类型,新类型中的属性都是可以为 null 的。

type MyNullable<T> = {
  [K in keyof T]: T[K] | null;
};

10.5 MyWritable 类型

目标:创建 MyWritable 类型,通过泛型参数接收旧的类型,返回新类型,新类型中的属性都是可写的。

interface User {
  readonly username: string;
}

type MyWritable<T> = {
  -readonly [K in keyof T]: T[K];
};

const userCanWrite: MyWritable<User> = {
  username: "李四",
};

userCanWrite.username = "王五";

10.6 MyNecessary 类型

目标:创建 MyNecessary 类型,通过泛型参数接收旧的类型,返回新类型,新类型中的属性都是必选的。

interface User {
  username?: string;
}

type MyNecessary<T> = {
  [K in keyof T]-?: T[K];
};

// 类型 "{}" 中缺少属性 "username", 但类型 "Necessary<User>" 中需要该属性
const user: MyNecessary<User> = {};

11. 工具类型

11.1 Partial

将类型中的属性都变成可选的,接收类型,返回类型。

Partial<Type>
interface Person {
  name: string;
  age: number;
}

type anothor = Partial<Person>;
// {name?: string, age?: number}
function updateObject<T>(obj: T, props: Partial<T>) {
  return { ...obj, ...props };
}

updateObject<Person>(person, { name: "李四" });

11.2 Readonly

将类型中的属性都变成只读的,接收类型,返回类型。

Readonly<Type>
interface Person {
  name: string;
  age: number;
}

// {readonly name: string, readonly age: number}
const anthor: Readonly<Person> = {
  name: "李四",
  age: 40,
};

// 不可以修改
// anthor.name = "赵六";

11.3 Record

Record<Keys, Type>
// 字典
let employees = {
  1: { id: 1, fullname: "John Doe", role: "Designer" },
  2: { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
  3: { id: 3, fullname: "Sara Duckson", role: "Developer" },
}
interface EmployeeType {
  id: number
  fullname: string
  role: string
}

let employees: Record<number, EmployeeType> = {
  1: { id: 1, fullname: "John Doe", role: "Designer" },
  2: { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
  3: { id: 3, fullname: "Sara Duckson", role: "Developer" },
}

11.4 Omit

接收类型,得到新类型,在新类型中不要包含 keys

Omit<Type, Keys>
interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}

/*
  type TodoPreview = {
    completed: boolean;
    createdAt: number;
  }
*/
// Omit 单词意为"省略"
type TodoPreview = Omit<Todo, "description" | "title">;

11.5 Pick

接收类型,返回新类型,在新类型中要包含 keys

Pick<Type, Keys>
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

/*
  type TodoPreview = {
    title: string;
    completed: boolean;
  }
*/
// Pick 单词意为"选择"
type TodoPreview = Pick<Todo, "title" | "completed">;

11.6 Exclude

接收联合类型,得到新类型,在新类型中排除联合类型中的 ExcludedMembers。

Exclude<UnionType, ExcludedMembers>
// type T0 = "b" | "c"
type T0 = Exclude<"a" | "b" | "c", "a">;
// type T1 = "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
  • 47
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹅£

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值