Typescript学习入门到精通(2023年超全)

706 阅读1小时+

前言

TypeScript 已经成为前端编程语言的事实标准。但我从大量的 Code Review 和面试经历中发现,真正能深入使用 TypeScript 的开发其实并不多。如果你不知道 ReturnType<T> 的作用和实现,或许这篇文章也适合你。

当然,我们花大量时间去学习一门语言或技术并非为了追求奇技淫巧,而是出于务实的态度。如果你看过大量使用 any 的 TypeScript 的代码,你肯定会感叹“那还不如不用”。在此种情况下,使用 TypeScript 的成本大于它所带来的收益。

如何充分利用好 TypeScript 的语言特性,帮助自己写出更健壮、类型提示更友好的代码?那就需要"再深入一些"。

为什么要使用Typescript

  • 首先你需要知道,编程开发中我们有一个共识:错误出现的越早越好
  1. 能在写代码的时候发现错误,就不要在代码编译时再发现(IDE的优势就是在代码编写过程中帮助我们发现错误)。

  2. 能在代码编译期间发现错误,就不要在代码运行期间再发现(类型检测就可以很好的帮助我们做到这一点)。

  3. 能在开发阶段发现错误,就不要在测试期间发现错误,能在测试期间发现错误,就不要在上线后发现错误。

  • 现在我们想探究的就是如何在 代码编译期间 发现代码的错误:

image.png

image.png - 这是我们一个非常常见的错误:

  • 这个错误很大的原因就是因为JavaScript没有对我们传入的参数进行任何的限制,只能等到运行期间才发现这个错误;并且当这个错误产生时,会影响后续代码的继续执行,也就是整个项目都因为一个小小的错误而深入崩溃;当然,你可能会想:我怎么可能犯这样低级的错误呢? 当我们写像我们上面这样的简单的demo时,这样的错误很容易避免,并且当出现错误时,也很容易检查出来; 但是当我们开发一个大型项目时呢?你能保证自己一定不会出现这样的问题吗?而且如果我们是调用别人的类库,又如何知道让我们传入的到底是什么样的参数呢?

◼ 但是,如果我们可以给JavaScript加上很多限制,在开发中就可以很好的避免这样的问题了:

 比如我们的getLength函数中str是一个必传的类型,没有调用者没有传编译期间就会报错;

 比如我们要求它的必须是一个String类型,传入其他类型就直接报错;

 那么就可以知道很多的错误问题在编译期间就被发现,而不是等到运行时再去发现和修改;

认识Typescript

  • TypeScript是拥有类型的JavaScript超集,它可以编译成普通、干净、完整的JavaScript代码。

  • 怎么理解上面的话呢?

 我们可以将TypeScript理解成加强版的JavaScript。

 JavaScript所拥有的特性,TypeScript全部都是支持的,并且它紧随ECMAScript的标准,所以ES6、ES7、ES8等新语法标准,它都是支持的;

 TypeScript在实现新特性的同时,总是保持和ES标准的同步甚至是领先;

 并且在语言层面上,不仅仅增加了类型约束,而且包括一些语法的扩展,比如枚举类型(Enum)、元组类型(Tuple)等;

 并且TypeScript最终会被编译成JavaScript代码,所以你并不需要担心它的兼容性问题,在编译时也可以不借助于Babel这样的工具;

◼ 所以,我们可以把TypeScript理解成更加强大的JavaScript,不仅让JavaScript更加安全,而且给它带来了诸多好用的好用特性;

但是每一样技术的出现都会让惊喜,因为他必然是解决了之前技术的某一个痛点的,而TypeScript真是解决了JavaScript存在的很多设计缺陷,尤其是关于类型检测的。

安装环境

`npm install -g  typescript // 全局安装 ts`
查看版本
tsc --version`

安装 ts-node

那么通过我们上面的一通操作,我们知道了运行tsc命令就可以编译生成一个js文件,但是如果每次改动我们都要手动去执行编译,然后再通过 node命令才能查看运行结果岂不是太麻烦了。

而 ts-node 正是来解决这个问题的

npm i -g ts-node // 全局安装ts-node

有了这个插件,我们就可以直接运行.ts文件了

我们试一下

ts-node helloworld.ts

变量的声明

  • 声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解(Type Annotation);
  • 声明格式
var/let/const 标识符: 数据类型 = 赋值;

let message: string = "Hello World"

注意:这里的string是小写的,和String是有区别的

 string是TypeScript中定义的字符串类型,String是ECMAScript中定义的一个类

◼ 如果我们给message赋值其他类型的值,那么就会报错:

image.png

TypeScript 基础类型

Number 类型

数字类型是我们开发中经常使用的类型,TypeScript和JavaScript一样,不区分整数类型(int)和浮点型(double),统一为number类型。

const count: number = 10;

boolean类型

image.png

String 类型

image.png

Array 类型

  • 明确的指定<数组>的类型注解: 两种写法
  1. string[]: 数组类型, 并且数组中存放的字符串类型
  2. Array: 数组类型, 并且数组中存放的是字符串类型
    let names: string[] = ["abc", "cba", "nba"]

image.png

object、Object 和 {}

另外,object(首字母小写,以下称“小 object”)、Object(首字母大写,以下称“大 Object”)和 {}(以下称“空对象”)

JavaScript 中以下类型被视为原始类型:stringbooleannumberbigintsymbolnull 和 undefined

小 object 代表的是所有非原始类型,也就是说我们不能把 number、string、boolean、symbol等 原始类型赋值给 object。在严格模式下,nullundefined 类型也不能赋给 object。

let object: object;
object = 1; // 报错
object = "a"; // 报错
object = true; // 报错
object = null; // 报错
object = undefined; // 报错
object = {}; // 编译正确

大 Object 代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object(严格模式下 null 和 undefined 不可以)

let bigObject: Object;
object = 1; // 编译正确
object = "a"; // 编译正确
object = true; // 编译正确
object = null; // 报错
ObjectCase = undefined; // 报错
ObjectCase = {}; // ok

注意:尽管官方文档说可以使用小 object 代替大 Object,但是我们仍要明白大 Object 并不完全等价于小 object。

  • {}空对象类型和大 Object 一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null 和 undefined 也不能赋给 {}

综上结论:{}、大 Object 是比小 object 更宽泛的类型(least specific),{} 和大 Object 可以互相代替,用来表示原始类型(null、undefined 除外)和非原始类型;而小 object 则表示非原始类型。

Symbol类型

image.png

undefined和null

默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给其他类型。

// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined

// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined

// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined

// null和undefined赋值给Symbol
let sym: symbol = Symbol("me"); 
sym = null
sym= undefined

// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined

// null和undefined赋值给bigint
let big: bigint =  100n;
big = null
big= undefined

如果你在tsconfig.json指定了"strictNullChecks":true ,null 和 undefined 只能给它们自己的类型赋值

// 启用 --strictNullChecks
let x: number;
x = 1; // 编译正确
x = undefined;    // 编译错误
x = null;    // 编译错误

函数

函数声明

  function add(x: number, y: number): number {
  return x + y;
}

函数表达式

  const add = function(x: number, y: number): number {
  return x + y;
}

函数的返回值类型

image.png

匿名函数的参数

image.png

对象类型

对象类型的简单回顾
const info: {
  name: string
  age: number
} = {
  name: "why",
  age: 18
}
对象类型和函数类型结合使用
type PointType = {//定义类型别名
  x: number
  y: number
  z?: number//z是可选参数
}
function printCoordinate(point: PointType) {
  console.log("x坐标:", point.x)
  console.log("y坐标:", point.y)
}

// printCoordinate(123)//传入的参数个数与定义的要一致
printCoordinate({ x: 20, y: 30 })

export {}

在对象我们可以添加属性,并且告知TypeScript该属性需要是什么类型;

属性之间可以使用, 或者; 来分割,最后一个分隔符是可选的;

每个属性的类型部分也是可选的,如果不指定,那么就是any类型;

◼对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?:

any类型

在某些情况下,我们确实无法确定一个变量的类型,并且可能它会发生一些变化,这个时候我们可以使用any类型

我们可以对any类型的变量进行任何的操作,包括获取不存在的属性、方法;

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

我们给一个any类型的变量赋值任何的值,比如数字、字符串的值;


let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。请记住,any 是魔鬼!尽量不要用any。

为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

unknown 类型

unknownany的最大区别是: 任何类型的值可以赋值给any,同时any类型的值也可以赋值给任何类型。unknown 任何类型的值都可以赋值给它,但它只能赋值给unknownany

let notSure: unknown = 4;
let uncertain: any = notSure; // OK

let notSure: any = 4;
let uncertain: unknown = notSure; // OK

let notSure: unknown = 4;
let uncertain: number = notSure; // Error

如果不缩小类型,就无法对unknown类型执行任何操作:

 function getDog() {
 return '123'
}
 
const dog: unknown = {hello: getDog};
dog.hello(); // Error

这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用typeof类型断言等方式来缩小未知范围:

 function getDogName() {
 let x: unknown;
 return x;
};
const dogName = getDogName();
    
// 直接使用
const upName = dogName.toLowerCase(); // Error
    
// typeof
if (typeof dogName === 'string') {
  const upName = dogName.toLowerCase(); // OK
}
    
// 类型断言 
const upName = (dogName as string).toLowerCase(); // OK

void类型

void表示没有任何类型,和其他类型是平等关系,不能直接赋值:

let a: void; 
let b: number = a; // Error

我们可以将undefined赋值给void类型,也就是函数可以返回undefined

你只能为它赋予nullundefined(在strictNullChecks未指定为true时)。声明一个void类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。

值得注意的是,方法没有返回值将得到undefined,但是我们需要定义成void类型,而不是undefined类型。否则将报错:

  function fun(): undefined {
  console.log("this is TypeScript");
};
fun(); // Error
 // 1.在TS中如果一个函数没有任何的返回值, 那么返回值的类型就是void类型
// 2.如果返回值是void类型, 那么我们也可以返回undefined(TS编译器允许这样做而已)
function sum(num1: number, num2: number): void {
  console.log(num1 + num2)

  // return 123 错误的做法
}

never型

never类型表示的是那些永不存在的值的类型。

值会永不存在的两种情况:

  1. 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了);
  2. 函数中执行无限循环的代码(死循环),使得程序永远无法运行到函数返回值那一步,永不存在返回。
// 异常
function err(msg: string): never { // OK
  throw new Error(msg); 
}

// 死循环
function loopForever(): never { // OK
  while (true) {};
}

never类型同nullundefined一样,也是任何类型的子类型,也可以赋值给任何类型。

let err: never;
let num: number = 4;

num = err; // OK

但是没有类型是never的子类型或可以赋值给never类型(除了never本身之外),即使any也不可以赋值给never

let ne: never;
let nev: never;
let an: any;

ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => { throw new Error("异常"); })(); // OK
ne = (() => { while(true) {} })(); // OK

在 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;
  }
}

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean;

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

Tuple(元组)

元祖定义

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组最重要的特性是可以限制数组元素的个数和类型,它特别适合用来实现多值返回。

元祖用于保存定长定数据类型的数据

let x: [string, number]; 
// 类型必须匹配且个数必须为2

x = ['hello', 10]; // OK 
x = ['hello', 10,10]; // Error 
x = [10, 'hello']; // Error

注意,元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。如果一个数组中可能有多种类型,数量和类型都不确定,那就直接any[]

元祖类型的解构赋值

我们可以通过下标的方式来访问元组中的元素,当元组中的元素较多时,这种方式并不是那么便捷。其实元组也是支持解构赋值的:

let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${id}`);
console.log(`username: ${username}`);

以上代码成功运行后,控制台会输出以下消息:

id: 1
username: Semlinker

这里需要注意的是,在解构赋值时,如果解构数组元素的个数是不能超过元组中元素的个数,否则也会出现错误,比如:

let employee: [number, string] = [1, "Semlinker"];\
let [id, username, age] = employee;

在以上代码中,我们新增了一个 age 变量,但此时 TypeScript 编译器会提示以下错误信息:

Tuple type '[number, string]' of length '2' has no element at index '2'.

很明显元组类型 [number, string] 的长度是 2,在位置索引 2 处不存在任何元素。

元组类型的可选元素

与函数签名类型,在定义元组类型时,我们也可以通过 ? 号来声明元组类型的可选元素,具体的示例如下:

let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${optionalTuple}`);

在上面代码中,我们定义了一个名为 optionalTuple 的变量,该变量的类型要求包含一个必须的字符串属性和一个可选布尔属性,该代码正常运行后,控制台会输出以下内容:

optionalTuple : Semlinker,true
optionalTuple : Kakuqo

那么在实际工作中,声明可选的元组元素有什么作用?这里我们来举一个例子,在三维坐标轴中,一个坐标点可以使用 (x, y, z) 的形式来表示,对于二维坐标轴来说,坐标点可以使用 (x, y) 的形式来表示,而对于一维坐标轴来说,只要使用 (x) 的形式来表示即可。针对这种情形,在 TypeScript 中就可以利用元组类型可选元素的特性来定义一个元组类型的坐标点,具体实现如下:

type Point = [number, number?, number?];

const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点

console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3

元组类型的剩余元素

元组类型里最后一个元素可以是剩余元素,形式为 ...X,这里 X 是数组类型。剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如,[number, ...string[]] 表示带有一个 number 元素和任意数量string 类型元素的元组类型。为了能更好的理解,我们来举个具体的例子:

type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);

只读的元组类型

TypeScript 3.4 还引入了对只读元组的新支持。我们可以为任何元组类型加上 readonly 关键字前缀,以使其成为只读元组。具体的示例如下:

const point: readonly [number, number] = [10, 20];

在使用 readonly 关键字修饰元组类型之后,任何企图修改元组中元素的操作都会抛出异常:

// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);

TypeScript语法细节

联合类型

◼TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型。

◼我们来使用第一种组合类型的方法:联合类型(Union Type)

联合类型是由两个或者多个其他类型组成的类型;

表示可以是这些类型中的任何一个值;

联合类型中的每一个类型被称之为联合成员(union's members);

function printId(id: number|string){
    console.log("你的id是:",id)
    }
printId(10)
printId("abc")

◼ 传入给一个联合类型的值是非常简单的:只要保证是联合类型中的某一个类型的值即可

 但是我们拿到这个值之后,我们应该如何使用它呢?因为它可能是任何一种类型。

 比如我们拿到的值可能是string或者number,我们就不能对其调用string上的一些方法;

◼ 那么我们怎么处理这样的问题呢?

 我们需要使用缩小(narrow)联合(后续我们还会专门讲解缩小相关的功能);

 TypeScript可以根据我们缩小的代码结构,推断出更加具体的类型;

function printID(id: number | string) {
  console.log("您的ID:", id)

  // 类型缩小
  if (typeof id === "string") {
    console.log(id.length)
  } else {
    console.log(id)
  }
}

printID("abc")
printID(123)

联合类型通常与 null 或 undefined 一起使用:

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

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

sayHello("semlinker"); 
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

以上示例中的 12 或 'click' 被称为字面量类型,用来约束取值只能是某几个值中的一个。

类型别名

◼ 在前面,我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次。

◼ 类型别名用来给一个类型起个新名字。它只是起了一个新名字,并没有创建新类型。类型别名常用于联合类型。

// 类型别名: type
type MyNumber = number
const age: MyNumber = 18

// 给ID的类型起一个别名
type IDType = number | string

function printID(id: IDType) {
  console.log(id)
}


// 打印坐标
type PointType = { x: number, y: number, z?: number }
function printCoordinate(point: PointType) {
  console.log(point.x, point.y, point.z)
}

接口(Interfaces)

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

什么是接口

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

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

简单的例子

interface Person {
    name: string;
    age: number;
}
let tom: Person = {
    name: 'Tom',
    age: 25
};

上面的例子中,我们定义了一个接口 Person,接着定义了一个变量 tom,它的类型是 Person。这样,我们就约束了 tom 的形状必须和接口 Person 一致。

接口一般首字母大写。

定义的变量比接口少了一些属性是不允许的:

interface Person {
    name: string;
    age: number;
}
let tom: Person = {
    name: 'Tom'
};

// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.

多一些属性也是不允许的:

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

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

可见,赋值的时候,变量的形状必须和接口的形状保持一致

interface和type区别

◼ 我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?

如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;

◼ 如果是定义对象类型,那么他们是有区别的:

  1. interface 可以重复的对某个接口来定义属性和方法;
  2. 而type定义的是别名,别名是不能重复的;
    
// 1.区别一: type类型使用范围更广, 接口类型只能用来声明对象
type MyNumber = number
type IDType = number | string


// 2.区别二: 在声明对象时, interface可以多次声明
// 2.1. type不允许两个相同名称的别名同时存在
// type PointType1 = {
//   x: number
//   y: number
// }

// type PointType1 = {
//   z?: number
// }


// 2.2. interface可以多次声明同一个接口名称
interface PointType2 {
  x: number
  y: number
}

interface PointType2 {
  z: number
}

const point: PointType2 = {
  x: 100,
  y: 200,
  z: 300
}


// 3.interface支持继承的
interface IPerson {
  name: string
  age: number
}

interface IKun extends IPerson {
  kouhao: string
}

const ikun1: IKun = {
  kouhao: "你干嘛, 哎呦",
  name: "kobe",
  age: 30
}

// 4.interface可以被类实现(TS面向对象时候再讲)
// class Person implements IPerson {

// }


// 总结: 如果是非对象类型的定义使用type, 如果是对象类型的声明那么使用interface


export {}


交叉类型

◼ 前面我们学习了联合类型:

 联合类型表示多个类型中一个即可

◼ 还有另外一种类型合并,就是交叉类型(Intersection Types):

 交叉类似表示需要满足多个类型的条件;

 交叉类型使用 & 符号;

◼ 我们来看下面的交叉类型:

 表达的含义是number和string要同时满足;

 但是有同时满足是一个number又是一个string的值吗?其实是没有的,所以MyType其实是一个never类型;

// 回顾: 联合类型
type ID = number | string
const id1: ID = "abc"
const id2: ID = 123

// 交叉类型: 两种(多种)类型要同时满足
type NewType = number & string // 没有意义

interface IKun {
  name: string
  age: number
}

interface ICoder {
  name: string
  coding: () => void
}

type InfoType = IKun & ICoder

const info: InfoType = {
  name: "why",
  age: 18,
  coding: function() {
    console.log("coding")
  }
}

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用&定义交叉类型。

{
  type Useless = string & number;
}

很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,在上述的代码中,类型别名 Useless 的类型就是个 never。

交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:

  type IntersectionType = { id: number; name: string; } & { age: number };
  const mixed: IntersectionType = {
    id: 1,
    name: 'name',
    age: 18
  }

在上述示例中,我们通过交叉类型,使得 IntersectionType 同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。

思考

这里,我们来发散思考一下:如果合并的多个接口类型存在同名属性会是什么效果呢?

如果同名属性的类型不兼容,比如上面示例中两个接口类型同名的 name 属性类型一个是 number,另一个是 string,合并后,name 属性的类型就是 number 和 string 两个原子类型的交叉类型,即 never,如下代码所示:

  type IntersectionTypeConfict = { id: number; name: string; } 
  & { age: number; name: number; };
  const mixedConflict: IntersectionTypeConfict = {
    id: 1,
    name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
    age: 2
  };

此时,我们赋予 mixedConflict 任意类型的 name 属性值都会提示类型错误。而如果我们不设置 name 属性,又会提示一个缺少必选的 name 属性的错误。在这种情况下,就意味着上述代码中交叉出来的 IntersectionTypeConfict 类型是一个无用类型。

如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型、数字字面量类型,合并后 name 属性的类型就是两者中的子类型。

如下所示示例中 name 属性的类型就是数字字面量类型 2,因此,我们不能把任何非 2 之外的值赋予 name 属性。

  type IntersectionTypeConfict = { id: number; name: 2; } 
  & { age: number; name: number; };

  let mixedConflict: IntersectionTypeConfict = {
    id: 1,
    name: 2, // ok
    age: 2
  };
  mixedConflict = {
    id: 1,
    name: 22, // '22' 类型不能赋给 '2' 类型
    age: 2
  };

那么如果同名属性是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:

interface A {
  x:{d:true},
}
interface B {
  x:{e:string},
}
interface C {
  x:{f:number},
}
type ABC = A & B & C
let abc:ABC = {
  x:{
    d:true,
    e:'',
    f:666
  }
}

以上代码成功运行后,会输出以下结果:

1634261312(1).png

由上图可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并

类型断言

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

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

TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)

其中,greaterThan2 一定是一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。

在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number。

不过,我们可以使用一种笃定的方式——类型断言(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。

比如,我们可以使用 as 语法做类型断言,如下代码所示:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;

语法

// 尖括号 语法
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;

以上两种方式虽然没有任何区别,但是尖括号格式会与react中JSX产生语法冲突,因此我们更推荐使用 as 语法。

示例

// 获取DOM元素 <img class="img"/>
// const imgEl = document.querySelector(".img")
// if (imgEl !== null) { // 类型缩小
//   imgEl.src = "xxx"
//   imgEl.alt = "yyy"
// }

// 使用类型断言
const imgEl = document.querySelector(".img") as HTMLImageElement
imgEl.src = "xxx"
imgEl.alt = "yyy"


// 类型断言的规则: 断言只能断言成更加具体的类型, 或者 不太具体(any/unknown) 类型
const age: number = 18
// 错误的做法
// const age2 = age as string

// TS类型检测来说是正确的, 但是这个代码本身不太正确
// const age3 = age as any
// const age4 = age3 as string
// console.log(age4.split(" "))


export {}

非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。

具体看以下示例:

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

确定赋值断言

允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:

let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
  x = 10;
}


很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。

◼ 当我们编写下面的代码时,在执行ts的编译阶段会报错:

image.png

 这是因为传入的message有可能是为undefined的,这个时候是不能执行方法的;

◼ 但是,我们确定传入的参数是有值的,这个时候我们可以使用非空类型断言:

非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测;

image.png

实例

// 定义接口
interface IPerson {
  name: string
  age: number
  friend?: {
    name: string
  }
}

const info: IPerson = {
  name: "why",
  age: 18
}

// 访问属性: 可选链: ?.
console.log(info.friend?.name)

// 属性赋值:
// 解决方案一: 类型缩小
if (info.friend) {
  info.friend.name = "kobe"
}

// 解决方案二: 非空类型断言(有点危险, 只有确保friend一定有值的情况, 才能使用)
info.friend!.name = "james"

export {}

字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。

目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:

{
  let specifiedStr: 'this is string' = 'this is string';
  let specifiedNum: 1 = 1;
  let specifiedBoolean: true = true;
}

比如 'this is string' (这里表示一个字符串字面量类型)类型是 string 类型(确切地说是 string 类型的子类型),而 string 类型不一定是 'this is string'(这里表示一个字符串字面量类型)类型,如下具体示例:

{
  let specifiedStr: 'this is string' = 'this is string';
  let str: string = 'any string';
  specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
  str = specifiedStr; // ok 
}

比如说我们用“马”比喻 string 类型,即“黑马”代指 'this is string' 类型,“黑马”肯定是“马”,但“马”不一定是“黑马”,它可能还是“白马”“灰马”。因此,'this is string' 字面量类型可以给 string 类型赋值,但是 string 类型不能给 'this is string' 字面量类型赋值,这个比喻同样适合于形容数字、布尔等其他字面量和它们父类的关系。

字符串字面量类型

一般来说,我们可以使用一个字符串字面量类型作为变量的类型,如下代码所示:

let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'

实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型(后面会讲解),用来描述拥有明确成员的实用的集合。

如下代码所示,我们使用字面量联合类型描述了一个明确、可 'up' 可 'down' 的集合,这样就能清楚地知道需要的数据结构了。

type Direction = 'up' | 'down';

function move(dir: Direction) {
  // ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'


通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。

因此,相较于使用 string 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。

数字字面量类型及布尔字面量类型

数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 Config:

interface Config {
    size: 'small' | 'big';
    isEnable:  true | false;
    margin: 0 | 2 | 4;
}

在上述代码中,我们限定了 size 属性为字符串字面量类型 'small' | 'big',isEnable 属性为布尔字面量类型 true | false(布尔字面量只包含 true 和 false,true | false 的组合跟直接使用 boolean 没有区别),margin 属性为数字字面量类型 0 | 2 | 4。

let和const分析

我们先来看一个 const 示例,如下代码所示:

{
  const str = 'this is string'; // str: 'this is string'
  const num = 1; // num: 1
  const bool = true; // bool: true
}

在上述代码中,我们将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定,这也是一种比较合理的设计。

接下来我们看看如下所示的 let 示例:

{

  let str = 'this is string'; // str: string
  let num = 1; // num: number
  let bool = true; // bool: boolean
}

在上述代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如 str 的类型是 'this is string' 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。

这种设计符合编程预期,意味着我们可以分别赋予 str 和 num 任意值(只要类型是 string 和 number 的子集的变量):

  str = 'any string';
  num = 2;
  bool = false;

我们将 TypeScript 的字面量子类型转换为父类型的这种设计称之为 "literal widening",也就是字面量类型的拓宽,比如上面示例中提到的字符串字面量类型转换成 string 类型,下面我们着重介绍一下。

实例

// 1.字面量类型的基本上
const name: "why" = "why"
let age: 18 = 18

// 2.将多个字面量类型联合起来 |
type Direction = "left" | "right" | "up" | "down"
const d1: Direction = "left"

// 栗子: 封装请求方法
type MethodType = "get" | "post"
function request(url: string, method: MethodType) {
}

request("http://codercba.com/api/aaa", "post")

// TS细节
// const info = {
//   url: "xxxx",
//   method: "post"
// }
// 下面的做法是错误: info.method获取的是string类型
// request(info.url, info.method)

// 解决方案一: info.method进行类型断言
// request(info.url, info.method as "post")

// 解决方案二: 直接让info对象类型是一个字面量类型
// const info2: { url: string, method: "post" } = {
//   url: "xxxx",
//   method: "post"
// }
const info2 = {
  url: "xxxx",
  method: "post"
} as const
// xxx 本身就是一个string
这是因为我们的对象在进行字面量推理的时候,info其实是一个 {url: string, method: string},
所以我们没办法将一个 string赋值给一个 字面量 类型。
request(info2.url, info2.method)

export {}


类型缩小(Type Narrowing)

实例

// 1.typeof: 使用的最多
function printID(id: number | string) {
  if (typeof id === "string") {
    console.log(id.length, id.split(" "))
  } else {
    console.log(id)
  }
}


// 2.===/!==: 方向的类型判断(平等缩小)
type Direction = "left" | "right" | "up" | "down"
function switchDirection(direction: Direction) {
  if (direction === "left") {
    console.log("左:", "角色向左移动")
  } else if (direction === "right") {
    console.log("右:", "角色向右移动")
  } else if (direction === "up") {
    console.log("上:", "角色向上移动")
  } else  {
    console.log("下:", "角色向下移动")
  }
}


// 3. instanceof: 传入一个日期, 打印日期
function printDate(date: string | Date) {
  if (date instanceof Date) {
    console.log(date.getTime())
  } else {
    console.log(date)
  }

  // if (typeof date === "string") {
  //   console.log(date)
  // } else {
  //   console.log(date.getTime())
  // }
}


// 4.in: 判断是否有某一个属性
interface ISwim {
  swim: () => void
}

interface IRun {
  run: () => void
}

function move(animal: ISwim | IRun) {
  if ("swim" in animal) {
    animal.swim()
  } else if ("run" in animal) {
    animal.run()
  }
}

const fish: ISwim = {
  swim: function() {}
}

const dog: IRun = {
  run: function() {}
}

move(fish)
move(dog)

在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 "Type Narrowing"。

比如,我们可以使用类型守卫(后面会讲到)将函数参数的类型从 any 缩小到明确的类型,具体示例如下:

{
  let func = (anything: any) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else if (typeof anything === 'number') {
      return anything; // 类型是 number
    }
    return null;
  };
}

在 VS Code 中 hover 到第 4 行的 anything 变量提示类型是 string,到第 6 行则提示类型是 number。

同样,我们可以使用类型守卫将联合类型缩小到明确的子类型,具体示例如下:

{
  let func = (anything: string | number) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else {
      return anything; // 类型是 number
    }
  };
}

当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:

{
  type Goods = 'pen' | 'pencil' |'ruler';
  const getPenCost = (item: 'pen') => 2;
  const getPencilCost = (item: 'pencil') => 4;
  const getRulerCost = (item: 'ruler') => 6;
  const getCost = (item: Goods) =>  {
    if (item === 'pen') {
      return getPenCost(item); // item => 'pen'
    } else if (item === 'pencil') {
      return getPencilCost(item); // item => 'pencil'
    } else {
      return getRulerCost(item); // item => 'ruler'
    }
  }
}

在上述 getCost 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if 语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。

那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenCost、getPencilCost、getRulerCost 呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === 'pencil' 的分支,item 的类型就被收缩为“pencil”。

事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:

  const getCost = (item: Goods) =>  {
    if (item === 'pen') {
      item; // item => 'pen'
    } else {
      item; // => 'pencil' | 'ruler'
    }
  }

一般来说 TypeScript 非常擅长通过条件来判别类型,但在处理一些特殊值时要特别注意 —— 它可能包含你不想要的东西!例如,以下从联合类型中排除 null 的方法是错误的:

const el = document.getElementById("foo"); // Type is HTMLElement | null
if (typeof el === "object") {
  el; // Type is HTMLElement | null
}

因为在 JavaScript 中 typeof null 的结果是 "object" ,所以你实际上并没有通过这种检查排除 null 值。除此之外,falsy 的原始值也会产生类似的问题:

function foo(x?: number | string | null{
  if (!x) {
    x; // Type is string | number | null | undefined\
  }
}

因为空字符串和 0 都属于 falsy 值,所以在分支中 x 的类型可能是 string 或 number 类型。帮助类型检查器缩小类型的另一种常见方法是在它们上放置一个明确的 “标签”:

interface UploadEvent {
  type"upload";
  filename: string;
  contents: string;
}

interface DownloadEvent {
  type"download";
  filename: string;
}

type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case "download":
      e; // Type is DownloadEvent 
      break;
    case "upload":
      e; // Type is UploadEvent 
      break;
  }
}

这种模式也被称为 ”标签联合“ 或 ”可辨识联合“,它在 TypeScript 中的应用范围非常广。

类型拓宽(Type Widening)

所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。

下面我们通过字符串字面量的示例来理解一下字面量类型拓宽:

  let str = 'this is string'; // 类型是 string
  let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
  const specifiedStr = 'this is string'; // 类型是 'this is string'
  let str2 = specifiedStr; // 类型是 'string'
  let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;

因为第 1~2 行满足了 let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为 string(形参类型确切地讲是 string | undefined)。

因为第 3 行的常量不可变更,类型没有拓宽,所以 specifiedStr 的类型是 'this is string' 字面量类型。

第 4~5 行,因为赋予的值 specifiedStr 的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2 的类型被推断为 'this is string',它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。

基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为。

{
  const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
  let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
}

实际上,除了字面量类型拓宽之外,TypeScript 对某些特定类型值也有类似 "Type Widening" (类型拓宽)的设计,下面我们具体来了解一下。

比如对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:

{
  let x = null; // 类型拓宽成 any
  let y = undefined; // 类型拓宽成 any

  /** -----分界线------- */
  const z = null; // 类型是 null

  /** -----分界线------- */
  let anyFun = (param = null) => param; // 形参类型是 null
  let z2 = z; // 类型是 null
  let x2 = x; // 类型是 null
  let y2 = y; // 类型是 undefined
}


注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成“any”。

为了更方便的理解类型拓宽,下面我们举个例子,更加深入的分析一下

假设你正在编写一个向量库,你首先定义了一个 Vector3 接口,然后定义了 getComponent 函数用于获取指定坐标轴的值:

interface Vector3 {
  xnumber;
  ynumber;
  znumber;
}

function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
  return vector[axis];
}

但是,当你尝试使用 getComponent 函数时,TypeScript 会提示以下错误信息:

let x = "x";
let vec = { x: 10, y: 20, z: 30 };
// 类型“string”的参数不能赋给类型“"x" | "y" | "z"”的参数。
getComponent(vec, x); // Error

为什么会出现上述错误呢?通过 TypeScript 的错误提示消息,我们知道是因为变量 x 的类型被推断为 string 类型,而 getComponent 函数期望它的第二个参数有一个更具体的类型。这在实际场合中被拓宽了,所以导致了一个错误。

这个过程是复杂的,因为对于任何给定的值都有许多可能的类型。例如:

const arr = ['x'1];

上述 arr 变量的类型应该是什么?这里有一些可能性:

  • ('x' | 1)[]
  • ['x', 1]
  • [string, number]
  • readonly [string, number]
  • (string | number)[]
  • readonly (string|number)[]
  • [any, any]
  • any[]

没有更多的上下文,TypeScript 无法知道哪种类型是 “正确的”,它必须猜测你的意图。尽管 TypeScript 很聪明,但它无法读懂你的心思。它不能保证 100% 正确,正如我们刚才看到的那样的疏忽性错误。

在下面的例子中,变量 x 的类型被推断为字符串,因为 TypeScript 允许这样的代码:

let x = 'semlinker';
x = 'kakuqo';
x = 'lolo';

对于 JavaScript 来说,以下代码也是合法的:

let x = 'x';
x = /x|y|z/;
x = ['x''y''z'];

在推断 x 的类型为字符串时,TypeScript 试图在特殊性和灵活性之间取得平衡。一般规则是,变量的类型在声明之后不应该改变,因此 string 比 string|RegExp 或 string|string[] 或任何字符串更有意义。

TypeScript 提供了一些控制拓宽过程的方法。其中一种方法是使用 const。如果用 const 而不是 let 声明一个变量,那么它的类型会更窄。事实上,使用 const 可以帮助我们修复前面例子中的错误:

const x = "x"; // type is "x" 
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK

因为 x 不能重新赋值,所以 TypeScript 可以推断更窄的类型,就不会在后续赋值中出现错误。因为字符串字面量型 “x” 可以赋值给  "x"|"y"|"z",所以代码会通过类型检查器的检查。

然而,const 并不是万灵药。对于对象和数组,仍然会存在问题

以下这段代码在 JavaScript 中是没有问题的:

const obj = { 
  x: 1,
}

obj.x = 6
obj.x = '6';

obj.y = 8;
obj.name = 'semlinker';

而在 TypeScript 中,对于 obj 的类型来说,它可以是 {readonly x:1} 类型,或者是更通用的 {x:number} 类型。当然也可能是 {[key: string]: number} 或 object 类型。对于对象,TypeScript 的拓宽算法会将其内部属性视为将其赋值给 let 关键字声明的变量,进而来推断其属性的类型。因此 obj 的类型为 {x:number} 。这使得你可以将 obj.x 赋值给其他 number 类型的变量,而不是 string 类型的变量,并且它还会阻止你添加其他属性。

因此最后三行的语句会出现错误:

const obj = { 
  x: 1,
};

obj.x = 6; // OK 


// Type '"6"' is not assignable to type 'number'.
obj.x = '6'; // Error

// Property 'y' does not exist on type '{ x: number; }'.
obj.y = 8; // Error

// Property 'name' does not exist on type '{ x: number; }'.
obj.name = 'semlinker'; // Error

TypeScript 试图在具体性和灵活性之间取得平衡。它需要推断一个足够具体的类型来捕获错误,但又不能推断出错误的类型。它通过属性的初始化值来推断属性的类型,当然有几种方法可以覆盖 TypeScript 的默认行为。一种是提供显式类型注释:

// Type is { x: 1 | 3 | 5; }
const obj: { x: 1 | 3 | 5 } = {
  x: 1 
};

另一种方法是使用 const 断言。不要将其与 let 和 const 混淆,后者在值空间中引入符号。这是一个纯粹的类型级构造。让我们来看看以下变量的不同推断类型:

// Type is { x: number; y: number; }
const obj1 = { 
  x: 1, 
  y: 2 
}

// Type is { x: 1; y: number; }
const obj2 = {
  x: 1 as const,
  y: 2,
}

// Type is { readonly x: 1; readonly y: 2; }
const obj3 = {
  x: 1, 
  y: 2 
} as const;

当你在一个值之后使用 const 断言时,TypeScript 将为它推断出最窄的类型,没有拓宽。对于真正的常量,这通常是你想要的。当然你也可以对数组使用 const 断言:

// Type is number[]
const arr1 = [123]

// Type is readonly [1, 2, 3]
const arr2 = [123] as const;

TypeScript函数类型

◼ 在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)。

◼ 那么在使用函数的过程中,函数是否也可以有自己的类型呢?

◼ 我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型;

实例

//函数类型的表达式
type CalcType = (num1: number, num2: number) => number

// 1.函数的定义
function calc(calcFn: CalcType) {
  const num1 = 10
  const num2 = 20
  const res = calcFn(num1, num2)
  console.log(res)
}


// 2.函数的调用
function sum(num1: number, num2: number) {
  return num1 + num2
}

function foo(num1: number) {
  return num1
}
calc(sum)
calc(foo)

function mul(num1: number, num2: number) {
  return num1 * num2
}
calc(mul)

// 3.使用匿名函数
calc(function(num1, num2) {
  return num1 - num2
})

export {}

image.png

函数声明

function sum(x: number, y: number): number {
    return x + y;
}

函数表达式

let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};

函数类型参数的格式

实例

// TypeScript对于传入的函数类型的多余的参数会被忽略掉(the extra arguments are simply ignored.)
type CalcType = (num1: number, num2: number) => number
function calc(calcFn: CalcType) {
  calcFn(10, 20)
}

calc(function(num) {
  return 123
})

// forEach栗子:
//本来forEach(function(item,index,arr){...},可以省略后面两个参数
    
const names = ["abc", "cba", "nba"]
names.forEach(function(item) {
  console.log(item.length)
})

// TS对于很多类型的检测报不报错, 取决于它的内部规则
// TS版本在不断更新: 在进行合理的类型检测的情况, 让ts同时更好用(好用和类型检测之间找到一个平衡)
// 举一个栗子:
interface IPerson {
  name: string
  age: number
}

// typescript github issue, 成员
const p = {
  name: "why",
  age: 18,
  height: 1.88,
  address: "广州市"
}

const info: IPerson = p

export {}


调用签名(Call Signatures)

实例

// 1.函数类型表达式
type BarType = (num1: number) => number

// 2.函数的调用签名(从对象的角度来看待这个函数, 也可以有其他属性)
interface IBar {
  name: string
  age: number
  // 函数可以调用: 函数调用签名
  //(参数列表):返回值类型
  (num1: number): number
}

const bar: IBar = (num1: number): number => {
  return 123
}

bar.name = "aaa"
bar.age = 18
bar(123)





export {}



开发中如何选择:

  1. 如果只是描述函数类型本身(函数可以被调用), 使用函数类型表达式(Function Type Expressions)
  2. 如果在描述函数作为对象可以被调用, 同时也有其他属性时, 使用函数调用签名(Call Signatures)

构造签名(Construct Signatures)(了解)

实例

class Person {
}

interface ICTORPerson {
  new (): Person
}

function factory(fn: ICTORPerson) {
  const f = new fn()
  return f
}

factory(Person)

函数的参数-可选参数

// y就是一个可选参数
// 可选参数类型是什么? 由number | undefined 联合类型组成
function foo(x: number, y?: number) {
  //类型缩小
  if (y !== undefined) {
    console.log(y + 10)
  }
}

foo(10)
foo(10, 20)

export {}


注意点:可选参数后面不允许再出现必需参数

函数的参数-参数默认值

// 函数的参数可以有默认值
// 1.有默认值的情况下, 参数的类型注解可以省略
// 2.有默认值的参数, 是可以接收一个undefined的值
function foo(x: number, y = 100) {
  console.log(y + 10)
}

foo(10)
foo(10, undefined)
foo(10, 55)

export {}


函数的参数-剩余参数

//实例一 
function foo(...args: (string | number)[]) {

}

foo(123, 321)
foo("abc", 111, "cba")
    
    
//实例二
function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}
let a = [];
push(a, 1, 2, 3);

函数的重载-案例分析

实例


// 需求: 只能将两个数字/两个字符串进行相加
// 案例分析: any实现
// function add(arg1, arg2) {
//   return arg1 + arg2
// }

// add(10, 20)
// add("abc", "cba")
// add({aaa: "aaa"}, 123)


// 1.实现两个函数
// function add1(num1: number, num2: number) {
//   return num1 + num2
// }

// function add2(str1: string, str2: string) {
//   return str1 + str2
// }

// add1(10, 20)
// add2("abc", "cba")


// 2.错误的做法: 联合类型是不可以
// function add(arg1: number|string, arg2: number|string) {
//   return arg1 + arg2
// }


// 3.TypeScript中函数的重载写法
// 3.1.先编写重载签名
function add(arg1: number, arg2: number): number
function add(arg1: string, arg2: string): string

// 3.2.编写通用的函数实现
function add(arg1: any, arg2: any): any {
  return arg1 + arg2
}

add(10, 20)
add("aaa", "bbb")
// 通用函数不能被调用
// add({name: "why"}, "aaa")
// add("aaa", 111)

export {}
  

由于 JavaScript 是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:

function add(x, y) {
 return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"

由于 TypeScript 是 JavaScript 的超集,因此以上的代码可以直接在 TypeScript 中使用,但当 TypeScript 编译器开启 noImplicitAny 的配置项时,以上代码会提示以下错误信息:

Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.

该信息告诉我们参数 x 和参数 y 隐式具有 any 类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add 函数同时支持 string 和 number 类型,因此我们可以定义一个 string | number 联合类型,同时我们为该联合类型取个别名:

type Combinable = string | number;

在定义完 Combinable 联合类型后,我们来更新一下 add 函数:

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

为 add 函数的参数显式设置类型之后,之前错误的提示消息就消失了。那么此时的 add 函数就完美了么,我们来实际测试一下:

const result = add('Semlinker', ' Kakuqo');
result.split(' ');
复制代码

在上面代码中,我们分别使用 'Semlinker' 和 ' Kakuqo' 这两个字符串作为参数调用 add 函数,并把调用结果保存到一个名为 result 的变量上,这时候我们想当然的认为此时 result 的变量的类型为 string,所以我们就可以正常调用字符串对象上的 split 方法。但这时 TypeScript 编译器又出现以下错误信息了:

Property 'split' does not exist on type 'number'.

很明显 number 类型的对象上并不存在 split 属性。问题又来了,那如何解决呢?这时我们就可以利用 TypeScript 提供的函数重载特性。

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

type Types = number | string
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:Types, b:Types) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}
const result = add('Semlinker', ' Kakuqo');
result.split(' ');

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string 类型。

函数重载-联合类型

实例

// 1.普通的实现
// function getLength(arg) {
//   return arg.length
// }

// 2.函数的重载
// function getLength(arg: string): number
// function getLength(arg: any[]): number
// function getLength(arg) {
//   return arg.length
// }


// 3.联合类型实现(可以使用联合类型实现的情况, 尽量使用联合类型)
// function getLength(arg: string | any[]) {
//   return arg.length
// }

// 4.对象类型实现
function getLength(arg: { length: number }) {
  return arg.length
}


getLength("aaaaa")
getLength(["abc", "cba", "nba"])
// getLength({ name: "why", length: 100 })

函数中this默认类型

实例

// 在没有对TS进行特殊配置的情况下, this是any类型

// 1.对象中的函数中的this
const obj = {
  name: "why",
  studying: function() {
    // 默认情况下, this是any类型
    console.log(this.name.length, "studying")
  }
}

obj.studying()
// obj.studying.call({})


// 2.普通的函数
function foo() {
  // console.log(this)
}

export {}

函数中this明确类型

image.png

image.png

实例

// 在设置配置选项(编译选项compilerOptions, noImplicitThis设置为true, 不允许模糊的this存在)

// 1.对象中的函数中的this
const obj = {
  name: "why",
  studying: function(this: {}) {
    // 默认情况下, this是any类型
    console.log(this, "studying")
  }
}

// obj.studying()
obj.studying.call({})


// 2.普通的函数
function foo(this: { name: string }, info: {name: string}) {
  console.log(this, info)
}

foo.call({ name: "why" }, { name: "kobe" })

export {}

this的内置工具使用

image.png

image.png

实例

function foo(this: { name: string }, info: {name: string}) {
  console.log(this, info)
}

type FooType = typeof foo

// 1.ThisParameterType: 获取FooType类型中this的类型
type FooThisType = ThisParameterType<FooType>


// 2.OmitOmitThisParameter: 删除this参数类型, 剩余的函数类型
type PureFooType = OmitThisParameter<FooType>


// 3.ThisType: 用于绑定一个上下文的this
interface IState {
  name: string
  age: number
}

interface IStore {
  state: IState
  eating: () => void
  running: () => void
}

const store: IStore & ThisType<IState> = {
  state: {
    name: "why",
    age: 18
  },
  eating: function() {
    console.log(this.name)
  },
  running: function() {
    console.log(this.name)
  }
}

store.eating.call(store.state)


export {}

TypeScript面向对象

认识类的使用

◼ 在早期的JavaScript开发中(ES5)我们需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以更加方

便的定义和使用类。

◼ TypeScript作为JavaScript的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测。

◼ 实际上在JavaScript的开发过程中,我们更加习惯于函数式编程:

 比如React开发中,目前更多使用的函数组件以及结合Hook的开发模式;

 比如在Vue3开发中,目前也更加推崇使用 Composition API;

◼ 但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。

◼ 类的定义我们通常会使用class关键字:

 在面向对象的世界里,任何事物都可以使用类的结构来描述;

 类中包含特有的属性和方法;

类的定义

◼ 我们来定义一个Person类:

 使用class关键字来定义一个类;

◼ 我们可以声明类的属性:在类的内部声明类的属性以及对应的类型

 如果类型没有声明,那么它们默认是any的;

 我们也可以给属性设置初始化值;

 在默认的strictPropertyInitialization模式下面我们的属性是必须

初始化的,如果没有初始化,那么编译时就会报错;

✓ 如果我们在strictPropertyInitialization模式下确实不希望给属

性初始化,可以使用 name!: string语法;

◼ 类可以有自己的构造函数constructor,当我们通过new关键字创建

一个实例时,构造函数会被调用;

 构造函数不需要返回任何值,默认返回当前创建出来的实例;

◼ 类中可以有自己的函数,定义的函数称之为方法;

class Person {
  // 成员属性: 声明成员属性
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  eating() {
    console.log(this.name + " eating")
  }

  running() {
    console.log(this.name + " running")
  }
}

// 实例对象: instance
const p1 = new Person("why", 18)
const p2 = new Person("kobe", 30)

console.log(p1.name, p2.age)

export {}


类的继承

面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。

◼ 我们使用extends关键字来实现继承,子类中使用super来访问父类。

◼ 我们来看一下Student类继承自Person:

 Student类可以有自己的属性和方法,并且会继承Person的属性和方法;

 在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化;

image.png

类的成员修饰符

◼ 在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected

 public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;

 private 修饰的是仅在同一类中可见、私有的属性或方法;

 protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;

实例

// public
// private
// protected

class Person {
  protected name: string
  private age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  // 方法变成私有方法: 只有在类内部才能访问
  private eating() {
    console.log("吃东西", this.age, this.name)
  }
}

const p = new Person("why", 18)
// console.log(p.name, p.age)
// p.name = "kobe"
// p.eating()

// 子类中是否可以访问
class Student extends Person {
  constructor(name: string, age: number) {
    super(name, age)
  }

  studying() {
    console.log("在学习", this.name)
  }
}

const stu = new Student("why", 18)

只读属性readonly

如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly:

实例

class Person {
  readonly name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

// 类和实例之间的关系(重要)
const p = new Person("why", 18)
console.log(p.name, p.age)

// p.name = "kobe" 只读属性不能进行写入操作
p.age = 20

export {}

getters/setters

在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器。

实例

class Person {
  // 私有属性: 属性前面会使用_
  private _name: string
  private _age: number

  constructor(name: string, age: number) {
    this._name = name
    this._age = age
  }

  running() {
    console.log("running:", this._name)
  }

  // setter/getter: 对属性的访问进行拦截操作
  set name(newValue: string) {
    this._name = newValue
  }

  get name() {
    return this._name
  }


  set age(newValue: number) {
    if (newValue >= 0 && newValue < 200){
     this._age = newValue
    }
  }

  get age() {
    return this._age
  }
}

const p = new Person("why", 100)
p.name = "kobe"
console.log(p.name)

p.age = -10
console.log(p.age)


export {}

参数属性(Parameter Properties)

◼ TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性。

 这些就被称为参数属性(parameter properties);

 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类

属性字段也会得到这些修饰符;

image.png

实例

class Person {
  // 语法糖
  constructor(public name: string, private _age: number, readonly height: number) {
  }

  running() {
    console.log(this._age, "eating")
  }
}

const p = new Person("why", 18, 1.88)
console.log(p.name, p.height)

// p.height = 1.98

export {}

抽象类abstract

◼ 我们知道,继承是多态使用的前提。

 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。

 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。

◼ 什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。

 抽象方法,必须存在于抽象类中;

 抽象类是使用abstract声明的类;

◼ 抽象类有如下的特点:

 抽象类是不能被实例的话(也就是不能通过new创建)

 抽象方法必须被子类实现,否则该类必须是一个抽象类;

实例

abstract class Shape {
  // getArea方法只有声明没有实现体
  // 实现让子类自己实现
  // 可以将getArea方法定义为抽象方法: 在方法的前面加abstract
  // 抽象方法必须出现在抽象类中, 类前面也需要加abstract
  abstract getArea()
}


class Rectangle extends Shape {
  constructor(public width: number, public height: number) {
    super()
  }

  getArea() {
    return this.width * this.height
  }
}

class Circle extends Shape {
  constructor(public radius: number) {
    super()
  }

  getArea() {
    return this.radius ** 2 * Math.PI
  }
}

class Triangle extends Shape {
  getArea() {
    return 100
  }
}


// 通用的函数
function calcArea(shape: Shape) {
  return shape.getArea()
}

calcArea(new Rectangle(10, 20))
calcArea(new Circle(5))
calcArea(new Triangle())

// 在Java中会报错: 不允许
calcArea({ getArea: function() {} })

// 抽象类不能被实例化
// calcArea(new Shape())
// calcArea(100)
// calcArea("abc")

TS中类型检测-鸭子类型

// TypeScript对于类型检测的时候使用的鸭子类型
// 鸭子类型: 如果一只鸟, 走起来像鸭子, 游起来像鸭子, 看起来像鸭子, 那么你可以认为它就是一只鸭子
// 鸭子类型, 只关心属性和行为, 不关心你具体是不是对应的类型

class Person {
  constructor(public name: string, public age: number) {}

  running() {}
}

class Dog {
  constructor(public name: string, public age: number) {}
  running() {}
}

function printPerson(p: Person) {
  console.log(p.name, p.age)
}

printPerson(new Person("why", 18))
// printPerson("abc")
printPerson({name: "kobe", age: 30, running: function() {}})
printPerson(new Dog("旺财", 3))

const person: Person = new Dog("果汁", 5)


export {}

TS中的类-具有类型特性

class Person {}

/**
 * 类的作用:
 *  1.可以创建类对应的实例对象
 *  2.类本身可以作为这个实例的类型
 *  3.类也可以当中有一个构造签名的函数
 */

const name: string = "aaa"
const p: Person = new Person()
function printPerson(p: Person) {}

function factory(ctor: new () => void) {}
factory(Person)

export {}

对象类型的属性修饰符(Property Modifiers)

◼ 对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。

◼ 可选属性(Optional Properties)

 我们可以在属性名后面加一个 ? 标记表示这个属性是可选的;

◼ 只读属性(Readonly Properties)

 在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为;

 但在类型检查的时候,一个标记为 readonly的属性是不能被写入的。

// 定义对象类型
type IPerson = {
  // 属性?: 可选的属性
  name?: string
  // readonly: 只读的属性
  readonly age: number
}

interface IKun {
  name?: string
  readonly slogan: string
}

const p: IPerson = {
  name: "why",
  age: 18
}

// p.age = 30


索引签名(Index Signatures)

◼ 什么是索引签名呢?

 有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征;

 这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型; ◼ 一个索引签名的属性类型必须是 string 或者是 number。

 虽然 TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型;(了解)

interface ICollection {
  // 索引签名
  [index: string]: number

  length: number
}

const names: number[] = [111, 222, 333]
console.log(names[0])
console.log(names[1])
console.log(names[2])


function iteratorCollection(collection: ICollection) {
  console.log(collection[0])
  console.log(collection[1])
}

// iteratorCollection(names)
// const tuple: [string, string] = ["why", "18"]
// iteratorCollection(tuple)

iteratorCollection({ name: 111, age: 18, length: 10 })

export {}

TS对象类型索引签名-基本使用

// interface IPerson {
//   name: string
//   age: number
//   height: number
// }

// const p: IPerson = {
//   name: "why",
//   age: 18,
//   height: 1.88
// }

// console.log(p.address)

// 1.索引签名的理解
// interface InfoType {
//   // 索引签名: 可以通过字符串索引, 去获取到一个值, 也是字符串
//   [key: string]: string
// }
// function getInfo(): InfoType {
//   const abc: any = "hahah"
//   return abc
// }


// const info = getInfo()
// const name = info["name"]
// console.log(name, info.age, info.address)


// 2.索引签名的案例
interface ICollection {
  [index: number]: string
  length: number
}

function printCollection(collection: ICollection) {
  for (let i = 0; i < collection.length; i++) {
    const item = collection[i]
    console.log(item.length)
  }
}

const array = ["abc", "cba", "nba"]
const tuple: [string, string] = ["why", "广州"]
printCollection(array)
printCollection(tuple)

export {}


TS对象类型索引签名-类型问题

interface IIndexType {
  // 返回值类型的目的是告知通过索引去获取到的值是什么类型
  // [index: number]: string
  // [index: string]: any
  [index: string]: string
}

// 索引签名: [index: number]: string
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: any: 没有报错
// 1.索引要求必须是字符串类型 names[0] => names["0"]
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: string: 会报错
// 严格字面量赋值检测: ["abc", "cba", "nba"] => Array实例 => names[0] names.forEach
// const names: IIndexType = ["abc", "cba", "nba"]
// names["forEach"] => function
// names["map/filter"] => function

export {}


TS对象类型索引签名-两个签名

interface IIndexType {
  // 两个索引类型的写法
  [index: number]: string
  [key: string]: any

  // 要求一:下面的写法不允许: 数字类型索引的类型, 必须是字符串类型索引的类型的 子类型
  // 结论: 数字类型必须是比如字符串类型更加确定的类型(需要是字符串类型的子类型)
  // 原因: 所有的数字类型都是会转成字符串类型去对象中获取内容
  // 数字0: number|string, 当我们是一个数字的时候, 既要满足通过number去拿到的内容, 不会和string 
    拿到的结果矛盾
  // 数字"0": string

  // 数字0: string
  // 数字"0": number|string
  // [index: number]: number|string
  // [key: string]: string

  // 要求二: 如果索引签名中有定义其他属性, 其他属性返回的类型, 必须符合string类型返回的属性
  // [index: number]: string
  // [key: string]: number|string

  // aaa: string
  // bbb: boolean 错误的类型
}

const names: IIndexType = ["abc", "cba", "nba"]
const item1 = names[0]
const forEachFn = names["forEach"]

names["aaa"]

export {}


接口继承

◼ 接口和类一样是可以进行继承的,也是使用extends关键字:

 并且我们会发现,接口是支持多继承的(类不支持多继承)

image.png

interface IPerson {
  name: string
  age: number
}

// 可以从其他的接口中继承过来属性
// 1.减少了相同代码的重复编写
// 2.如果使用第三库, 给我们定义了一些属性
//  > 自定义一个接口, 同时你希望自定义接口拥有第三方某一个类型中所有的属性
//  > 可以使用继承来完成
interface IKun extends IPerson {
  slogan: string
}

const ikun: IKun = {
  name: "why",
  age: 18,
  slogan: "你干嘛, 哎呦"
}

export {}

接口的实现

◼ 接口定义后,也是可以被类实现的:

 如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入;

 这就是面向接口开发;

interface IKun {
  name: string
  age: number
  slogan: string

  playBasketball: () => void
}

interface IRun {
  running: () => void
}


const ikun: IKun = {
  name: "why",
  age: 18,
  slogan: "你干嘛!",
  playBasketball: function() {}
}

// 作用: 接口被类实现
class Person implements IKun, IRun {

  name: string
  age: number
  slogan: string

  playBasketball() {
    
  }

  running() {

  }
}

const ikun2 = new Person()
const ikun3 = new Person()
const ikun4 = new Person()
console.log(ikun2.name, ikun2.age, ikun2.slogan)
ikun2.playBasketball()
ikun2.running()

严格的字面量赋值检测

interface IPerson {
  name: string
  age: number
}


// 1.奇怪的现象一: 
// 定义info, 类型是IPerson类型
const obj = {
  name: "why",
  age: 18,

  // 多了一个height属性
  height: 1.88
}
const info: IPerson = obj


// 2.奇怪的现象二:
function printPerson(person: IPerson) {

}
const kobe = { name: "kobe", age: 30, height: 1.98 }
printPerson(kobe)


// 解释现象
// 第一次创建的对象字面量, 称之为fresh(新鲜的)
// 对于新鲜的字面量, 会进行严格的类型检测. 必须完全满足类型的要求(不能有多余的属性)
const obj2 = {
  name: "why",
  age: 18,

  height: 1.88
}

const p: IPerson = obj2

export {}

为什么会出现这种情况呢?

image.png ◼ 简单对上面的英文进行翻译解释:

 每个对象字面量最初都被认为是“新鲜的(fresh)”。

 当一个新的对象字面量分配给一个变量或传递给一个非空目标类型的参数时,对象字面量指定目标类型中不存在的属性是错误的。

 当类型断言或对象字面量的类型扩大时,新鲜度会消失。

TypeScript枚举类型

◼ 枚举类型是为数不多的TypeScript特性有的特性之一:

 枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型;

 枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型;

TS中枚举类型基本使用

// 定义枚举类型
enum Direction {
  LEFT,
  RIGHT
}

const d1: Direction = Direction.LEFT

function turnDirection(direction: Direction) {
  switch(direction) {
    case Direction.LEFT:
      console.log("角色向左移动一个格子")
      break
    case Direction.RIGHT:
      console.log("角色向右移动一个格子")
      break
  }
}

// 监听键盘的点击
turnDirection(Direction.LEFT)

export {}


TS中枚举类型设置值

// 定义枚举类型
// enum Direction {
//   LEFT = 0,
//   RIGHT = 1
// }

// enum Direction {
//   LEFT = 100,
//   RIGHT
// }

enum Direction {
  LEFT = "LEFT",
  RIGHT = "RIGHT"
}

enum Operation {
  Read = 1 << 0,
  Write = 1 << 1,
  foo = 1 << 2
}

const d1: Direction = Direction.LEFT

export {}

◼ 枚举类型默认是有值的,比如上面的枚举,默认值是这样的:

◼ 当然,我们也可以给枚举其他值:

 这个时候会从100进行递增;

◼ 我们也可以给他们赋值其他的类型:

image.png

TypeScript泛型编程

认识泛型

◼ 软件工程的主要目的是构建不仅仅明确和一致的API,还要让你的代码具有很强的可重用性:

 比如我们可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作;

 但是对于参数的类型是否也可以参数化呢?

◼ 什么是类型的参数化?

 我们来提一个需求:封装一个函数,传入一个参数,并且返回这个参数;

◼ 如果我们是TypeScript的思维方式,要考虑这个参数和返回值的类型需要一致:

image.png ◼ 上面的代码虽然实现了,但是不适用于其他类型,比如string、boolean、Person等类型:

image.png

泛型实现类型参数化

◼ 虽然any是可以的,但是定义为any的时候,我们其实已经丢失了类型信息:

 比如我们传入的是一个number,那么我们希望返回的可不是any类型,而是number类型;

 所以,我们需要在函数中可以捕获到参数的类型是number,并且同时使用它来作为返回值的类型;

◼ 我们需要在这里使用一种特性的变量 - 类型变量(type variable),它作用于类型,而不是值:

image.png ◼ 这里我们可以使用两种方式来调用它:

 方式一:通过 <类型> 的方式将类型传递给函数;

 方式二:通过类型推导(type argument inference),自动推到出我们传入变量的类型:

✓ 在这里会推导出它们是 字面量类型的,因为字面量类型对于我们的函数也是适用的

image.png

// 1.理解形参和实例参数化, 但是参数的类型是固定的
// function foo(name: string, age: number) {

// }
// foo("why", 19)
// foo("kobe", 30)


// 2.定义函数: 将传入的内容返回
// number/string/{name: string}
function bar<Type>(arg: Type): Type {
  return arg
}

// 2.1. 完整的写法
const res1 = bar<number>(123)
const res2 = bar<string>("abc")
const res3 = bar<{name: string}>({ name: "why" })

// 2.2. 省略的写法
const res4 = bar("aaaaaaaaa")
const res5 = bar(11111111)

// let message = "Hello World"

泛型的基本补充

◼ 当然我们也可以传入多个类型:

◼ 平时在开发中我们可能会看到一些常用的名称:

 T:Type的缩写,类型

 K、V:key和value的缩写,键值对

 E:Element的缩写,元素

 O:Object的缩写,对象

function foo<T, E>(arg1: T, arg2: E) {

}

foo(10, 20)
foo(10, "abc")
foo<string, { name: string }>("abc", { name: "why" })

export {}

泛型接口

image.png

image.png

//这里的string表示默认值为string
interface IKun<Type = string> {
  name: Type
  age: number
  slogan: Type
}

//这里的string表示传入给接口的类型Type为string
const kunkun: IKun<string> = {
  name: "why",
  age: 18,
  slogan: "哈哈哈"
}

const ikun2: IKun<number> = {
  name: 123,
  age: 20,
  slogan: 666
}

const ikun3: IKun = {
  name: "kobe",
  age: 30,
  slogan: "坤坤加油!"
}


export {}

泛型-泛型类的使用

class Point<Type = number> {
  x: Type
  y: Type
  constructor(x: Type, y: Type) {
    this.x = x
    this.y = y
  }
}
//const p1 = new Point<number>(10, 20)
const p1 = new Point(10, 20)
console.log(p1.x)
// 类型推导
// 原来const p2 = new Point<string>("123", "321")
const p2 = new Point("123", "321")
console.log(p2.x)

export {}

泛型约束(Generic Constraints)

◼ 有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:

 比如string和array都是有length的,或者某些对象也是会有length属性的;

 那么只要是拥有length的属性都可以作为我们的参数类型,那么应该如何操作呢?

image.png

这里表示是传入的类型必须有这个属性,也可以有其他属性,但是必须至少有这个成员。

 interface ILength {
  length: number
}

// 1.getLength没有必要用泛型
function getLength(arg: ILength) {
  return arg.length
}

const length1 = getLength("aaaa")
const length2 = getLength(["aaa", "bbb", "ccc"])
const length3 = getLength({ length: 100 })


// 2.获取传入的内容, 这个内容必须有length属性
// Type相当于是一个变量, 用于记录本次调用的类型, 所以在整个函数的执行周期中, 一直保留着参数的类型
function getInfo<Type extends ILength>(args: Type): Type {
  return args
}

const info1 = getInfo("aaaa")
const info2 = getInfo(["aaa", "bbb", "ccc"])
const info3 = getInfo({ length: 100 })

// getInfo(12345)
// getInfo({})

export {}

   

◼ 在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)

 你可以声明一个类型参数,这个类型参数被其他类型参数约束;

◼ 举个栗子:我们希望获取一个对象给定属性名的值

 我们需要确保我们不会获取 obj 上不存在的属性;

 所以我们在两个类型之间建立一个约束;

image.png

// 传入的key类型, obj当中key的其中之一
interface IKun {
  name: string
  age: number
}
//keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
type IKunKeys = keyof IKun // "name"|"age"的联合类型

function getObjectProperty<O, K extends keyof O>(obj: O, key: K){
  return obj[key]
}

const info = {
  name: "why",
  age: 18,
  height: 1.88
}

const name = getObjectProperty(info, "name")

export {}

映射类型(Mapped Types)

◼ 有的时候,一个类型需要基于另外一个类型,但是你又不想拷贝一份,这个时候可以考虑使用映射类型。

 大部分内置的工具都是通过映射类型来实现的;

 大多数类型体操的题目也是通过映射类型完成的;

◼ 映射类型建立在索引签名的语法上:

 映射类型,就是使用了 PropertyKeys 联合类型的泛型;

 其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型;

image.png

// TypeScript提供了映射类型: 函数
// 映射类型不能使用interface定义
// Type = IPerson
// keyof = "name" | "age"
type MapPerson<Type> = {
  // 索引类型以此进行使用
  [aaa in keyof Type]: Type[aaa]

  // name: string
  // age: number
}


interface IPerson {
  name: string
  age: number
}

// 拷贝一份IPerson
// interface NewPerson {
//   name: string
//   age: number
// }
type NewPerson = MapPerson<IPerson>


export {}


映射修饰符(Mapping Modifiers)

◼ 在使用映射类型时,有两个额外的修饰符可能会用到:

 一个是 readonly,用于设置属性只读;

 一个是 ? ,用于设置属性可选;

◼ 你可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀。

type MapPerson<Type> = {
  readonly [Property in keyof Type]?: Type[Property]
}

interface IPerson {
  name: string
  age: number
  height: number
  address: string
}

type IPersonOptional = MapPerson<IPerson>

const p: IPersonOptional = {

}

export {}


image.png

映射类型-修饰符符号

type MapPerson<Type> = {
  -readonly [Property in keyof Type]-?: Type[Property]
}

interface IPerson {
  name: string
  age?: number
  readonly height: number
  address?: string
}

// 
type IPersonRequired = MapPerson<IPerson>

const p: IPersonRequired = {
  name: "why",
  age: 18,
  height: 1.88,
  address: "广州市"
}


export {}


image.png

TypeScript知识扩展

TypeScript模块化

◼ JavaScript 有一个很长的处理模块化代码的历史,TypeScript 从 2012 年开始跟进,现在已经实现支持了很多格式。但是随着

时间流逝,社区和 JavaScript 规范已经使用为名为 ES Module的格式,这也就是我们所知的 import/export 语法。

 ES 模块在 2015 年被添加到 JavaScript 规范中,到 2020 年,大部分的 web 浏览器和 JavaScript 运行环境都已经广泛支持。

 所以在TypeScript中最主要使用的模块化方案就是ES Module;

image.png

tsconfig.json

认识tsconfig.json文件

◼ 什么是tsconfig.json文件呢?(官方的解释)

 当目录中出现了 tsconfig.json 文件,则说明该目录是 TypeScript 项目的根目录;

 tsconfig.json 文件指定了编译项目所需的根目录下的文件以及编译选项。

tsconfig.json配置

◼ 官方的解释有点“官方”,直接看我的解释。

◼ tsconfig.json文件有两个作用:

 作用一(主要的作用):让TypeScript Compiler在编译的时候,知道如何去编译TypeScript代码和进行类型检测;

✓ 比如是否允许不明确的this选项,是否允许隐式的any类型;

✓ 将TypeScript代码编译成什么版本的JavaScript代码;

 作用二:让编辑器(比如VSCode)可以按照正确的方式识别TypeScript代码;

✓ 对于哪些语法进行提示、类型错误检测等等;

◼ JavaScript 项目可以使用 jsconfig.json 文件,它的作用与 tsconfig.json 基本相同,只是默认启用了一些 JavaScript 相关的编译选项。 ◼ tsconfig.json在编译时如何被使用呢?

 在调用 tsc 命令并且没有其它输入文件参数时,编译器将由当前目录开始向父级目录寻找包含 tsconfig 文件的目录。

 调用 tsc 命令并且没有其他输入文件参数,可以使用 --project (或者只是 -p)的命令行选项来指定包含了 tsconfig.json 的

目录;

 当命令行中指定了输入文件参数, tsconfig.json 文件会被忽略;

◼ webpack中使用ts-loader进行打包时,也会自动读取tsconfig文件,根据配置编译TypeScript代码。

◼ tsconfig.json文件包括哪些选项呢?

 tsconfig.json本身包括的选项非常非常多,我们不需要每一个都记住;

 可以查看文档对于每个选项的解释:www.typescriptlang.org/tsconfig

 当我们开发项目的时候,选择TypeScript模板时,tsconfig文件默认都会帮助我们配置好的;

tsconfig.json介绍

tsconfig.json 是 TypeScript 项目的配置文件。如果一个目录下存在一个 tsconfig.json 文件,那么往往意味着这个目录就是 TypeScript 项目的根目录。

tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。

tsconfig.json 重要字段

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

compilerOptions 选项

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

image.png

image.png

编写高效 TS 代码的一些建议

尽量减少重复代码

对于刚接触 TypeScript 的小伙伴来说,在定义接口时,可能一不小心会出现以下类似的重复代码。比如:

interface Person {
  firstNamestring;
  lastNamestring;
}

interface PersonWithBirthDate {
  firstNamestring;
  lastNamestring;
  birthDate;
}

很明显,相对于 Person 接口来说,PersonWithBirthDate 接口只是多了一个 birth 属性,其他的属性跟 Person 接口是一样的。那么如何避免出现例子中的重复代码呢?要解决这个问题,可以利用 extends 关键字:

interface Person { 
  firstName: string; 
  lastName: string;
}

interface PersonWithBirthDate extends Person { 
  birth: Date;
}

当然除了使用 extends 关键字之外,也可以使用交叉运算符(&):

type PersonWithBirthDate = Person & { birth: Date };

另外,有时候你可能还会发现自己想要定义一个类型来匹配一个初始配置对象的「形状」,比如:

const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: "#00FF00",
  label: "VGA",
};

interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}

其实,对于 Options 接口来说,你也可以使用 typeof 操作符来快速获取配置对象的「形状」:

type Options = typeof INIT_OPTIONS;

在实际的开发过程中,重复的类型并不总是那么容易被发现。有时它们会被语法所掩盖。比如有多个函数拥有相同的类型签名:

function get(url: string, opts: Options): Promise<Response> { /* ... */ } 
function post(url: string, opts: Options): Promise<Response> { /* ... */ }

对于上面的 get 和 post 方法,为了避免重复的代码,你可以提取统一的类型签名:

type HTTPFunction = (url: string, opts: Options) => Promise<Response>; 
const getHTTPFunction = (url, opts) => { /* ... */ };
const postHTTPFunction = (url, opts) => { /* ... */ };

使用更精确的类型替代字符串类型

假设你正在构建一个音乐集,并希望为专辑定义一个类型。这时你可以使用 interface 关键字来定义一个 Album 类型:

interface Album {
  artist: string// 艺术家
  title: string// 专辑标题
  releaseDate: string// 发行日期:YYYY-MM-DD
  recordingType: string// 录制类型:"live" 或 "studio"
}

对于 Album 类型,你希望 releaseDate 属性值的格式为 YYYY-MM-DD,而 recordingType 属性值的范围为 live 或 studio。但因为接口中 releaseDate 和 recordingType 属性的类型都是字符串,所以在使用 Album 接口时,可能会出现以下问题:

const dangerous: Album = {
  artist: "Michael Jackson",
  title: "Dangerous",
  releaseDate: "November 31, 1991"// 与预期格式不匹配
  recordingType: "Studio"// 与预期格式不匹配
};

虽然 releaseDate 和 recordingType 的值与预期的格式不匹配,但此时 TypeScript 编译器并不能发现该问题。为了解决这个问题,你应该为 releaseDate 和 recordingType 属性定义更精确的类型,比如这样:

interface Album {\
  artiststring// 艺术家
  titlestring// 专辑标题
  releaseDateDate// 发行日期:YYYY-MM-DD
  recordingType"studio" | "live"// 录制类型:"live" 或 "studio"
}

重新定义 Album 接口之后,对于前面的赋值语句,TypeScript 编译器就会提示以下异常信息:

const dangerous: Album = {
  artist: "Michael Jackson",
  title: "Dangerous",
  // 不能将类型“string”分配给类型“Date”。ts(2322)
  releaseDate: "November 31, 1991", // Error
  // 不能将类型“"Studio"”分配给类型“"studio" | "live"”。ts(2322)\
  recordingType: "Studio", // Error
};

为了解决上面的问题,你需要为 releaseDate 和 recordingType 属性设置正确的类型,比如这样:

const dangerous: Album = {
  artist: "Michael Jackson",
  title: "Dangerous",
  releaseDate: new Date("1991-11-31"),
  recordingType: "studio",
};

定义的类型总是表示有效的状态

假设你正在构建一个允许用户指定页码,然后加载并显示该页面对应内容的 Web 应用程序。首先,你可能会先定义 State 对象:

interface State {
  pageContent: string;
  isLoading: boolean;
  errorMsg?: string;
}

接着你会定义一个 renderPage 函数,用来渲染页面:

function renderPage(state: State) {
  if (state.errorMsg) {
    return `呜呜呜,加载页面出现异常了...${state.errorMsg}`;
  } else if (state.isLoading) {
    return `页面加载中~~~`;
  }
  return `<div>${state.pageContent}</div>`;
}

// 输出结果:页面加载中~~~
console.log(renderPage({isLoading: true, pageContent: ""}));
// 输出结果:<div>大家好</div>
console.log(renderPage({isLoading: false, pageContent: "大家好呀"}));

创建好 renderPage 函数,你可以继续定义一个 changePage 函数,用于根据页码获取对应的页面数据:

async function changePage(state: State, newPage: string) {
  state.isLoading = true;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
    }
    const text = await response.text();
    state.isLoading = false;
    state.pageContent = text;
  } catch (e) {
    state.errorMsg = "" + e;
  }
}

对于以上的 changePage 函数,它存在以下问题:

  • 在 catch 语句中,未把 state.isLoading 的状态设置为 false
  • 未及时清理 state.errorMsg 的值,因此如果之前的请求失败,那么你将继续看到错误消息,而不是加载消息。

出现上述问题的原因是,前面定义的 State 类型允许同时设置 isLoading 和 errorMsg 的值,尽管这是一种无效的状态。针对这个问题,你可以考虑引入可辨识联合类型来定义不同的页面请求状态:

interface RequestPending {
  state: "pending";
}

interface RequestError {
  state: "error";
  errorMsg: string;
}

interface RequestSuccess {
  state: "ok";
  pageContent: string;
}

type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
  currentPage: string;
  requests: { [page: string]: RequestState };
}

在以上代码中,通过使用可辨识联合类型分别定义了 3 种不同的请求状态,这样就可以很容易的区分出不同的请求状态,从而让业务逻辑处理更加清晰。接下来,需要基于更新后的 State 类型,来分别更新一下前面创建的 renderPage 和 changePage 函数:

更新后的 renderPage 函数

function renderPage(state: State) {
  const { currentPage } = state;
  const requestState = state.requests[currentPage];
  switch (requestState.state) {
    case "pending":
      return `页面加载中~~~`;
    case "error":
      return `呜呜呜,加载第${currentPage}页出现异常了...${requestState.errorMsg}`;
    case "ok":
      `<div>第${currentPage}页的内容:${requestState.pageContent}</div>`;
  }
}

更新后的 changePage 函数

async function changePage(state: State, newPage: string) {
  state.requests[newPage] = { state: "pending" };
  state.currentPage = newPage;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`无法正常加载页面 ${newPage}: ${response.statusText}`);
    }
    const pageContent = await response.text();
    state.requests[newPage] = { state: "ok", pageContent };
  } catch (e) {
    state.requests[newPage] = { state: "error", errorMsg: "" + e };
  }
}

在 changePage 函数中,会根据不同的情形设置不同的请求状态,而不同的请求状态会包含不同的信息。这样 renderPage 函数就可以根据统一的 state 属性值来进行相应的处理。因此,通过使用可辨识联合类型,让请求的每种状态都是有效的状态,不会出现无效状态的问题。

参考

2022 typescript史上最强学习入门文章(2w字)