了不起的TypeScript

1,319 阅读1小时+

前言:

一篇完整的TypeScript入门教程,之前学习TS的时候写过三篇学习笔记,可惜太杂乱,今天将其推到了重写,并且借鉴了掘金上的一些文章,全文总共2.2w字,基本上涵盖了新手学习TS需要的各种知识点,当然,这只是基础,如果想要在项目中使用TS,那么看完这篇后可以看我的另一篇文章在React中拥抱TypeScript

一、学习TypeScript的意义

image.png

  • 为JavaScript提供可选的类型系统
  • 兼容当前及未来的JavaScript的特性

1. 强类型 VS 弱类型

我们都知道JavaScript是一门弱类型的语言,也就是说,我们定义一个变量,不需要考虑它的类型,比如我们定义一个变量a,给它赋值为1

let a = 1

然后我们可以把它赋值为一个字符串、数组、对象等等任意类型

a = 'zgc'
a = [1,3,4]
a = {name: "zgc"}

这样做虽然写起来很爽,不会报错,但是会有很多隐患。如果代码出现问题,很难找到问题出在哪里。

强类型语言的特点就是不允许改变变量的数据类型,除非进行强制类型转换

弱类型的一个特点就是在计算时,不同类型之间对使用者透明地对变量进行隐式转换。 也就是说变量可以被赋予不同的数据类型

2. 静态语言 VS 动态语言

都说JavaScript是动态语言,而Java是静态语言,那他们的区别到底是什么呢?

一门语言在编译时报错,那么是静态语言,如果在运行时报错,那么是动态语言

用官方一点的话说就是

编译阶段确定所有变量的类型的是静态类型语言;在执行阶段确定所有变量的类型的是动态类型语言。

所以说,类型有利于代码的重构,它有利于编译器在编译时而不是运行时捕获错误。我们在编写代码的时候就能避免很多错误,这样可以提高我们编码(找bug)的效率!

最后来个对比表格

静态类型语言动态类型语言
对类型极度严格对类型非常宽松
立即发现错误不能立即发现(单元测试)
运行时性能好运行时性能差(可以改善 v8)
自文档化可读性差(工具生成文档)

image.png

二、TypeScript特性

TypeScript作为JavaScript的超集,TypeScript究竟比JavaScript多了哪些特性

image.png

  • 相较于JS而言,TS拥有了静态类型,更加严格的语法,更强大的功能;
  • TS可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率;
  • TS代码可以编译为任意版本的JS代码,可有效解决不同JS运行环境的兼容问题;
  • 同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS却远远胜于JS。

image.png

也就是说我们编写的TypeScript代码,最终要编译成任意版本的JavaScript,这样就可以在任何可以使用JavaScript的地方使用它了~

三、搭建TypeScript环境

TS代码需要通过编译器编译为JS,然后再交由JS解析器执行,所以我们要搭建TypeScript环境来写我们的TS代码

npm install -g typescript

安装ts-node

npm i -g ts-node

创建一个 tsconfig.json 文件

tsc --init

然后新建index.ts,输入相关练习代码,然后执行

ts-node index.ts

四、 初识TS语法

  • 类型声明是TS非常重要的一个特点
  • 通过类型声明可以指定TS中变量(参数、形参)的类型
  • 指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错
  • 简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值

我们先来看看,TypeScript给变量指定类型的语法(类型注解)是什么

// 声明一个变量
let 变量: 类型

// 声明一个变量并给其赋值
let 变量: 类型 = 值

// 声明一个函数
function zgc(参数: 参数类型): 函数返回值类型{ 
    //... 
}

这里我们可以看到,就是在变量后面就一个冒号,然后标识其类型就可以了

五、类型

下面我们就来介绍介绍,TypeScript中到底有哪些类型

学过js你认识的类型:booleannumberstringundefinednullsymbolbigintobject

你可能不认识的类型:voidanyunknownnever

我们后面会详细的介绍这些你可能不认识的类型

1. 变量

如果我们在声明变量指定类型后,之后如果给变量赋值为其他类型的值,编辑器会报错,如图所示

let n: number = 1
n = 3
n = '1' // 编辑器报错,不能将字符串赋值给number类型的变量

2. JS类型推断

  • TS拥有自动的类型判断机制
  • 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型
  • 所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明

先看例子:

let str: string = '我的大刀早已饥渴难耐!'; // let str: string
let num: number = 250; // let num: number
let bool: boolean = false; // let bool: boolean

const str: string = '我的大刀早已饥渴难耐!'; // const str: string
const num: number = 250; // const num: number
const bool: boolean = false; // const bool: boolean

上面的栗子中,使用 let 定义变量时,我们写明了类型注解,因为值可能会改变。可是,使用 const 常量时还需要写明类型注解,有没有觉得有点麻烦?好在 TS 已经考虑到了这个问题。

在很多情况下,TS 会根据上下文环境自动地推断出变量的类型,无需我们再写明类型注解。上面的栗子可以简化:

let str = '我的大刀早已饥渴难耐!'; // 同上
let num = 250; // 同上
let bool = false; // 同上

const str = '我的大刀早已饥渴难耐!'; // const str: "我的大刀早已饥渴难耐!"
const num = 250; // const num: 250
const bool = false; // const bool: false
  //不懂得可以看下面类型拓宽

我们把 TypeScript 这种基于赋值表达式推断类型的能力称之为类型推断

在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。比如我们能根据 return 语句推断函数返回的类型,如下代码所示:

{
  /** 根据参数的类型,推断出返回值的类型也是 number */
  function add1(a: number, b: number) {
    return a + b;
  }
  const x1= add1(1, 1); // 推断出 x1 的类型也是 number
  
  /** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
  function add2(a: number, b = 1) {
    return a + b;
  }
  const x2 = add2(1);
  const x3 = add2(1, '1'); // ts(2345) Argument of type "1" is not assignable to parameter of type 'number | undefined
}

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

3. JS的内置类型

let str: string = "jimmy";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol("me"); 

null和undefined:

默认情况下 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 只能赋值给 void 和它们各自的类型。

number和bigint:

虽然numberbigint都表示数字,但是这两个类型不兼容。

let big: bigint =  100n;
let num: number = 6;
big = num;
num = big;

会抛出一个类型不兼容的 ts(2322) 错误。

4. 类型详解

(1)Array

对数组类型的定义有两种方式:

let arr:string[] = ["1","2"];
let arr2:Array<string> = ["1","2"];

定义联合类型数组:

let arr:(number | string)[];
// 表示定义了一个名称叫做arr的数组, 
// 这个数组中将来既可以存储数值类型的数据, 也可以存储字符串类型的数据
arr = [1, 'b', 2, 'c'];
--------------------------------------------------------------------------
let arr:Array<string|number> = [1,2,"aaa"]

定义指定对象成员的数组:

// type alias 类型别名
type User = { name: string; age: number };
//数组内的值为对象
const Arr: User[] = [
  {
    name: "zgc",
    age: 19,
  },
  {
    name: "zc",
    age: 1,
  },
];

(2)函数

函数声明

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

函数表达式

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

箭头函数

const sum = (x: number, y: number): number => x + y; 

可选参数

function queryUserInfo(name: string, age?: number) {
    if (age) {
        return `我叫${name},${age}岁`;
    }
    return `我叫${name},年龄保密`;
}

queryUserInfo('王思聪', 18); // 我叫王思聪,18岁(有钱人永远18岁!)
queryUserInfo('孙一宁'); // 我叫孙一宁,年龄保密

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

// 报错:A required parameter cannot follow an optional parameter
function queryUserInfo(name: string, age?: number, sex: string) {
    ...
}

参数默认值

function queryUserInfo(name: string, age: number, sex: string = '不详') {
    return `姓名:${name},年龄:${age},性别:${sex}`; 
}

queryUserInfo('xxx', 26); // 姓名:xxx,年龄:26,性别:不详

注意:  有默认值的参数也可放置在必需参数的前面,如果想要触发这个参数的默认值,必须要主动的传入undefined才可以。

剩余参数

function useDrop() {
  function push(array: any[], ...items: any[]) {
    items.forEach(function (item) {
      array.push(item);
    });
  }
  let a: any[] = [];
  push(a, 1, 2, 3);
  console.log(a); //[1,2,3]

  return <>1</>;
}
export default useDrop;

函数重载

由于 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 类型。

总结

  • 函数类型不建议直接给 Function 类型,有明确的参数类型、个数与返回值类型最佳。
type FunctionTypes = {
   onSomething: Function; // ❌ bad,不推荐。任何可调用的函数
   onClick: () => void; // ✅ better ,明确无参数无返回值的函数
   onChange: (id: number) => void; // ✅ better ,明确参数无返回值的函数
   onClick(event: React.MouseEvent<HTMLButtonElement>): void; // ✅ better
};

(3)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);

(4)void

相信熟悉Java或者C的同学应该对void也不会陌生,当函数没有返回值的时候,定义函数的返回值类型就是void

其实我们知道,在JavaScript中,如果定义一个函数不写返回值,那么它默认是返回 undefined 的~

什么都不返回

function fn(): void {
}

只写一个return

function fn(): void {
  return 
}

返回一个undefined

function fn(): void {
  return undefined
}

我们返回一个null也是可以的

function fn(): void {
  return null
}

除了以上的几种情况,其他情况都是不行的

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

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

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

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

function fun(): undefined {
  console.log("this is TypeScript");
};
fun(); // Error

(5)never

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

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

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

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

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

但是没有类型是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 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

(6)any

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

  • any 表示的是任意类型,一个变量设置类型为 any 后,相当于对该变量关闭了TS的类型检测。
  • 隐式any:如果只声明变量不指定类型不给变量赋值,变量就是any类型的
  • 当把any类型的变量可以赋值给其他类型的变量,不会报错 如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.

但如果是 any 类型,则允许被赋值为任意类型。

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

在any上访问任何属性都是允许的,也允许调用任何方法.

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

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

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

any类型是多人协作项目的大忌,很可能把Typescript变成AnyScript,通常在不得已的情况下,不应该首先考虑使用此类型。

(7)unknown

unknownany一样,所有类型都可以分配给unknown:

let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK

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类型断言等方式来缩小未知范围:

const a: unknown = '超神!';

// 直接使用
a.split(''); // error

// typeof
if (typeof a === 'string') {
    a.split(''); // ok
}

// 类型断言,后面会讲到
(a as string).split(''); // ok

(8)object、Object、{}

  • object:以下称小object
  • Object:以下称大Object
  • {}:以下称空对象 小object代表的是所有非原始类型,也就是说我们不能把number string等原始类型赋值给小object。在严格模式下,nullundefined类型也不能赋值给小object
以下类型被视为原始类型:stringnumberbooleannullundefined、bigInt、symbol

看例子:

let obj: object;

obj = 1; // error
obj = '人在塔在!'; // error
obj = true; // error
obj = null; // error
obj = undefined; // error
obj = 100n; // error
obj = Symbol(); // error
obj = {}; // ok

大Object代表所有拥有toString hasOwnProperty方法的类型,所以,所有原始类型和非原始类型都可以赋值给大Object。同样,在严格模式下nullundefined类型也不能赋给大Object

let obj: Object;

obj = 1; // ok
obj = '人在塔在!'; // ok
obj = true; // ok
obj = null; // error
obj = undefined; // error
obj = 100n; // ok
obj = Symbol(); // ok
obj = {}; // ok

从上面的栗子中可以看出,大Object包含原始类型,而小object仅包含非原始类型。你可能会想,那么大Object是不是小object的父类型?实际上,大Object不仅是小object的父类型,同时也是小object的子类型。为了证明这一点,我们举个🌰:

type FatherType = object extends Object ? true : false; // true
type ChildType  = Object extends object ? true : false; // true

注意: 尽管官网文档上说可以使用小object代替大Object,但是我们任需知道它们之间的区别。

空对象大Object可以互相代替,它们两的特性一致。

实操

一般来说,我们对于对象的类型检查一般是检查对象中有哪些属性,属性是什么类型。

可以用{} 来指定对象中可以包含哪些属性 语法:{属性名: 属性类型, ...}, 属性多了少了都不行

可选属性,在属性名后面加一个 ?

let obj: {name: string, age?: number}
obj = {name: 'zgc', age: 18}
obj = {name: 'zgc'}

如果后续属性不确定叫什么名字,也不确定是什么类型,也不确定有几个,那么可以这样写[propName: string]: unknown

// 定义对象结构
let obj: { name: string, [propName: string]: unknown }

obj = {name: 'yk', age: 18, gender: 'male'}
  • 一般你知道确切的属性类型,这没什么好说的。
type ObjectTypes = {
  obj3: {
    id: string;
    title: string;
  };
  objArr: {
    id: string;
    title: string;
  }[]; // 对象数组,or Array<{ id: string, title: string }>
};

let obj: ObjectTypes = {
  obj3: {
    id: "001",
    title: "1111",
  },
  objArr: [
    {
      id: "002",
      title: "2222",
    },
    {
      id: "003",
      title: "3333",
    },
  ],
};
  • 但有时你只知道是个对象,而不确定具体有哪些属性时,你可能会这么用:
type ObjectTypes = {
    obj: object; // ❌ bad,不推荐
    obj2: {}; // ❌ bad 几乎类似 object
};
  • 一般编译器会提示你,不要这么使用,推荐使用 Record
type ObjectTypes = {
    objBetter: Record<string, unknown>; // ✅ better,代替 obj: object
    
    // 对于 obj2: {}; 有三种情况:
    obj2Better1: Record<string, unknown>; // ✅ better 同上
    obj2Better2: unknown; // ✅ any value
    obj2Better3: Record<string, never>; // ✅ 空对象
    
    /** Record 更多用法 */
    dict1: {
        [key: string]: MyTypeHere;
    };
    dict2: Record<string, MyTypeHere>; // 等价于 dict1
};
export default function App() {
  type ObjectTypes = {
    objBetter: Record<string, unknown>; // ✅ better,代替 obj: object
    // 对于 obj2: {}; 有三种情况:
    obj2Better1: Record<string, unknown>; // ✅ better 同上
  };
  const zgc: ObjectTypes = {
    objBetter: { true: 2, true1: 2 },
    obj2Better1: { true: 2, true1: 2 },
  };
  console.log(zgc);

  return <div>1</div>;
}
  • Record 有什么好处呢,先看看实现:
// 意思就是,泛型 K 的集合作为返回对象的属性,且值类型为 T
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
  • 官方的一个例子
interface PageInfo {
    title: string;
}

type Page = 'home' | 'about' | 'contact';

const nav: Record<Page, PageInfo> = {
    about: { title: 'about' },
    contact: { title: 'contact' },
    // TS2322: Type '{ about: { title: string; }; contact: { title: string; }; hoem: { title: string; }; }' 
    // is not assignable to type 'Record<Page, PageInfo>'. ...
    hoem: { title: 'home' },
};

nav.about;

好处:

  1. 当你书写 home 值时,键入 h 常用的编辑器有智能补全提示;
  2. home 拼写错误成 hoem,会有错误提示,往往这类错误很隐蔽;
  3. 收窄接收的边界。

5. 字面量类型

在 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' 字面量类型赋值,这个比喻同样适合于形容数字、布尔等其他字面量和它们父类的关系。

(1)字符串字面量类型

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

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 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。

(2)数字字面量类型及布尔字面量类型

数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 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。

6. 联合类型

联合类型表示取值可以为多种类型中的一种,使用 | 分隔每个类型。

此时gender只能被赋值成这两个值中的一个 这个时候,字面量类型就有用了

let gender : "male" | "female"
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK

联合类型通常与 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' 被称为字面量类型,用来约束取值只能是某几个值中的一个。

一般的联合类型,没什么好说的,这里提一下非常有用,但新手经常遗忘的写法 —— 字符字面量联合。

  • 例如:自定义 ajax 时,一般 method 就那么具体的几种:getpostput 等。

大家都知道需要传入一个 string 型,你可能会这么写:

type UnionsTypes = {
    method: string; // ❌ bad,可以传入任意字符串
};
  • 使用字符字面量联合类型,第一、可以智能提示你可传入的字符常量;第二、防止拼写错误。后面会有更多的例子。
type UnionsTypes = {
    method: 'get' | 'post'; // ✅ good 只允许 'get'、'post' 字面量
};

7. 类型别名

这个时候,如果一个类型的字面量类型选择过多,重复编写就比较麻烦,这就可以使用类型别名

type myType = 1 | 2 | number[] | 4 | string;
let i: myType;
let j: myType;
let k: myType;

k = 2;
type sumType = {
  money: number;
  count: (value: number) => void;
};

let sum: sumType = {
  money: 200,
  // count(value: number): void{
  // this.money +=value
  //},简写
  count: function (value: number): void {
    this.money += value;
  },
};

type handleType = {
  name: string;
  sum: sumType;
  friends: Array<string>;
  //friends:string[]
};

let handle: handleType = {
  name: "z",
  sum: sum,
  friends: ["g", "c"],
};

handle.sum.count(500);

类型别名用来给一个类型起个新名字。类型别名常用于联合类型。

注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。

8. 枚举

比如我们在定义一个对象的时候

let person = { name: 'yk', gender: 'male' }

gender这种属性的取值一般只有两个,所以在存储的时候用数字来代替,可以提高效率,可以定义0表示女性,1表示男性

let person = { name: 'yk', gender: 1 }

但是这样可读性变差了,而且还不方便管理,这就引出我们今天要谈的枚举类

枚举就是一组有名字的常量集合

我们再举一个形象一点的例子,我们手机里都存储了各种朋友的手机号,记住手机号和人的对应关系可太难了,通讯录像是一个枚举类,将人名与手机号做映射,我们只需要看人名就可以了~

(1)默认枚举【数字枚举】

可以定义一个Gender枚举类

enum Gender {
  'male',
  'female'
}

怎么使用呢,就和使用对象中的属性一样Gender.male

let person = { name: 'zgc', gender: Gender.male }

枚举类中,将这些属性都默认定义成了数字,默认是从0开始

我们看看这个枚举类转义成JS是什么 www.typescriptlang.org/play

"use strict";
var Gender;
(function (Gender) {
    Gender[Gender["male"] = 0] = "male";
    Gender[Gender["female"] = 1] = "female";
})(Gender || (Gender = {}));

通过看这个js代码,我们可以发现,这不是简单的赋值为0,而是套了一层,也就是说,这个枚举类是支持正反映射的,什么意思呢

不但可以通过Gender.male取到0 还可以通过Gender[0]取到male

console.log(Gender.male) // 0
console.log(Gender['male']) // 0
console.log(Gender[0]) // male

(2)自定义枚举【字符串枚举】

默认的枚举类型的值都是数字类型的,所以我们称之为数字枚举,默认是从0开始递增的,我们也可以自定义数字,如果只定义第一个,后面的数字就依据你定义的递增

image.png

当然不止数字类型,也可以定义成字符串类型

enum Gender {
  'male'= 'this is male.',
  'female' = 'this is female'
}

这样就提供了有意义且可调试的字符串,因此可以被更容易地处理和调试

image.png

我买看转义成js是什么样的

image.png

这就和数字枚举就不太一样了,不能通过属性值反向取到属性名了

当然也可以将两种类型混合定义【异构枚举】

enum Gender {
  'male'= 'this is male.',
  'female' = 1
}

(3)常量枚举

enum前面加上const,表示常量枚举

我们先看普通的枚举类型转义之后是什么

image.png

再看看常量枚举转义之后是什么

image.png

可以看到,代码一下子精简了很多。定义的枚举类编译之后直接以值(常量)的形式来使用

9. 总结

image.png

10. 类型拓宽(Type Widening)

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 类型,下面我们着重介绍一下。 所有通过 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 = (str2 = specifiedStr) => str2; // 类型是 (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 {
  x: number;
  y: number;
  z: number;
}

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

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

let x = "x";
let vec = { x10y20z30 };
// 类型“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 = { x10y20z30 };
getComponent(vec, x); // OK

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

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

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

const obj = { 
  x1,
}; 

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 = { 
  x1,
};

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 的默认行为。

  • 一种是提供显式类型注释
const obj: { x: number | string; y?: number; name?: string } = {
    x: 1,
  };

  obj.x = 6;
  obj.x = "6";

  obj.y = 8;
  obj.name = "semlinker";
  • 另一种方法是使用 const 断言: 不要将其与 let 和 const 混淆,后者在值空间中引入符号。这是一个纯粹的类型级构造。让我们来看看以下变量的不同推断类型:
// Type is { x: number; y: number; }
const obj1 = { 
  x1, 
  y2 
}; 

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

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

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

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

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

既然有类型拓宽,自然也会有类型缩小,下面我们简单介绍一下 Type Narrowing。

11. 类型缩小(Type Narrowing)

在 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 中的应用范围非常广。

12. 交叉类型

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

{
  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
  }
}

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

1640058590(1).png

13. 类型断言

有时候你会遇到这样的情况,你会比 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;

(1)使用语法

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

(2)非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 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
}

(3)确定赋值断言

允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 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 编译器就会知道该属性会被明确地赋值。

六、接口(Interfaces)

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

1. 什么是接口

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

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

2. 简单的例子

interface Iobj {
  name: string;
  age: number;
  getName: (name: string) => string; //参数是string,返回值是string
}

var obj: Iobj = {
  name: "kerwin",
  age: 100,
  getName: (name) => {
    return name;
  },
};

obj.getName("aaaa");

接口一般首字母大写。

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

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'.

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

3. 可选 | 只读属性

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

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

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

4. 任意属性

有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

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

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

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

//不能将类型“{ name: string; age: number; gender: string; }”分配给类型“Person”。
//属性“age”与索引签名不兼容。
//不能将类型“number”分配给类型“string”。ts(2322)

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

interface Person {
    name: string;
    age?: number; // 这里真实的类型应该为:number | undefined
    [propName: string]: string | number | undefined;
}

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

5. 绕开额外属性检查的方式

(1)鸭式辨型法

鸭式辨型法像鸭子一样走路并且嘎嘎叫的就叫鸭子,即具有鸭子特征的认为它就是鸭子,也就是通过制定规则来判定对象是否实现这个接口。

例子:

interface LabeledValue {
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj); // OK
interface LabeledValue {
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}
printLabel({ size: 10, label: "Size 10 Object" }); // Error

上面代码,在参数里写对象就相当于是直接给labeledObj赋值,这个对象有严格的类型定义,所以不能多参或少参。

而当你在外面将该对象用另一个变量myObj接收,myObj不会经过额外属性检查,但会根据类型推论为let myObj: { size: number; label: string } = { size: 10, label: "Size 10 Object" };,然后将这个myObj再赋值给labeledObj,此时根据类型的兼容性,两种类型对象,参照鸭式辨型法,因为都具有label属性,所以被认定为两个相同,故而可以用此法来绕开多余的类型检查。

(2)类型断言

类型断言的意义就等同于你在告诉程序,你很清楚自己在做什么,此时程序自然就不会再进行额外的属性检查了。

interface Props { 
  name: string; 
  age: number; 
  money?: number;
}

let p: Props = {
  name: "兔神",
  age: 25,
  money: -100000,
  girl: false
} as Props; // OK

(3)索引签名

interface Props { 
  name: string; 
  age: number; 
  money?: number;
  [key: string]: any;
}

let p: Props = {
  name: "兔神",
  age: 25,
  money: -100000,
  girl: false
}; // OK

七、接口与类型别名的区别

实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 而接口的作用就是为这些类型命名和为你的代码或第三方代码定义数据模型。

type(类型别名)会给一个类型起个新名字。 type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 --它创建了一个新名字来引用那个类型。给基本类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

1. Objects / Functions

两者都可以用来描述对象或函数的类型,但是语法不同。

Interface

interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

Type alias

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

2. Other Types

与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组。

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

// dom
let div = document.createElement('div');
type B = typeof div;

3. 接口可以定义多次,类型别名不可以

与类型别名不同,接口可以定义多次,会被自动合并为单个接口。

interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };

4. 扩展

两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,同理,类型别名也可以扩展接口。

接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。

接口扩展接口

interface Obj1 {
    x: string;
}

interface Obj2 extends Obj1 {
    y: number;
}

const obj: Obj2 = {
    x: '生与死,轮回不止。我们生,他们死!',
    y: 555,
}

类型别名扩展类型别名

type Obj1 = {
    x: string;
}

type Obj2 = Obj1 & {
    y: number;
}

const obj: Obj2 = {
    x: '黑夜,就是我的舞台',
    y: 777
}

接口扩展类型别名

type Obj1 = {
    x: string;
}

interface Obj2 extends Obj1 {
    y: number;
}

const obj: Obj2 = {
    x: '我的一个跟斗,能翻十万八千里',
    y: 222,
}

类型别名扩展接口

interface Obj1 {
    x: string;
}

type Obj2 = Obj1 & {
    y: number;
}

const obj: Obj2 = {
    x: '只要点一下就够了,蠢货!',
    y: 333,
}

八、泛型

1. 泛型介绍

假如让你实现一个函数 identity,函数的参数可以是任何值,返回值就是将参数原样返回,并且其只能接受一个参数,你会怎么做?

你会觉得这很简单,顺手就写出这样的代码:

const identity = (arg) => arg;

由于其可以接受任意值,也就是说你的函数的入参和返回值都应该可以是任意类型。 现在让我们给代码增加类型声明:

type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;
...

一个笨的方法就像上面那样,也就是说 JS 提供多少种类型,就需要复制多少份代码,然后改下类型签名。这对程序员来说是致命的。这种复制粘贴增加了出错的概率,使得代码难以维护,牵一发而动全身。并且将来 JS 新增新的类型,你仍然需要修改代码,也就是说你的代码对修改开放,这样不好。还有一种方式是使用 any 这种“万能语法”。缺点是什么呢?我举个例子:

identity("string").length; // ok
identity("string").toFixed(2); // ok
identity(null).toString(); // ok
...

如果你使用 any 的话,怎么写都是 ok 的, 这就丧失了类型检查的效果。实际上我知道我传给你的是 string,返回来的也一定是 string,而 string 上没有 toFixed 方法,因此需要报错才是我想要的。也就是说我真正想要的效果是:当我用到id的时候,你根据我传给你的类型进行推导。比如我传入的是 string,但是使用了 number 上的方法,你就应该报错。

为了解决上面的这些问题,我们使用泛型对上面的代码进行重构。和我们的定义不同,这里用了一个 类型 T,这个 T 是一个抽象类型,只有在调用的时候才确定它的值,这就不用我们复制粘贴无数份代码了。

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

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

来张图片帮助你理解 image.png

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}
console.log(identity<number, string>(68, "Semlinker"));

除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}
console.log(identity(68, "Semlinker"));

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。

2. 泛型约束

假如我想打印出参数的 size 属性呢?如果完全不进行约束 TS 是会报错的:

function trace<T>(arg: T): T {
  console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
  return arg;
}

报错的原因在于 T 理论上是可以是任何类型的,不同于 any,你不管使用它的什么属性或者方法都会报错(除非这个属性和方法是所有集合共有的)。那么直观的想法是限定传给 trace 函数的参数类型应该有 size 类型,这样就不会报错了。如何去表达这个类型约束的点呢?实现这个需求的关键在于使用类型约束。 使用 extends 关键字可以做到这一点。简单来说就是你定义一个类型,然后让 T 实现这个接口即可。

interface Sizeable {
  size: number;
}
function trace<T extends Sizeable>(arg: T): T {
  console.log(arg.size);
  return arg;
}

有的人可能说我直接将 Trace 的参数限定为 Sizeable 类型可以么?如果你这么做,会有类型丢失的风险,不推荐。

3. 泛型工具类型

本篇为TypeScript基础知识的学习,这个知识点可以看我另一篇文章 在React中拥抱TypeScript,在其中我详细的介绍了泛型工具类型的使用。

九、tsconfig.json

1. tsconfig.json 介绍

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

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

2. tsconfig.json 重要字段

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

3. tsconfig.json 配置选项

官方文档 TypeScript: TSConfig Reference - Docs on every TSConfig option (typescriptlang.org)

{
  // 此json文件中可以写注释!
  /*
    tsconfig.json是ts编译器的配置文件,ts编译器可以根据它的信息来对代码进行编译
  */

  /*
    "include" 用来指定哪些ts文件需要被编译
      默认:当前路径下所有文件, ***
      路径:** 表示任意目录
            * 表示任意文件
  */
  "include": [
    "./src/**/*"
  ],
  
  /*
    "exclude" 不需要被编译的文件目录
    默认值:
      ["node_modules", "bower_components", "jspm_packages", "./dist"]
  */
  "exclude": [
    "./src/exclude/**/*"
  ],
  
  /*
    被继承的配置文件
    例如:"extends": "。/configs/base",
  */
  //  "extends": "",
  
  /*
    指定被编译文件的列表,只有需要编译的文件少时才会用到
  */
  //  "files": [],
  
  /*
    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          // 为装饰器提供元数据的支持
  }
}

(1)其他配置

标题功能
allowJs是否对js文件编译,默认值:false
checkJs是否对js文件进行语法检查,默认值:false
removeComments是否删除注释,默认值:false
noEmit不生成编译后的文件,默认值:false
noEmitOnError当有错误的时候不生成编译后的文件,默认值:false
sourceMap是否生成sourceMap,默认值:false

(2)严格检查

标题功能
strict启用所有的严格检查,设置后相当于开启了所有的严格检查,默认值:false
alwaysStrict总是以严格模式对代码进行编译,默认值:false
noImplicitAny禁止隐式的any类型,默认值:false
noImplicitThis禁止类型不明确的this,默认值:false
strictBindCallApply严格检查bind、call和apply的参数列表,默认值:false
strictFunctionTypes严格检查函数的类型,默认值:false
strictNullChecks严格的空值检查,默认值:false
strictPropertyInitialization严格检查属性是否初始化,默认值:false

(3)额外检查

标题功能
noFallthroughCasesInSwitch检查switch语句包含正确的break
noImplicitReturns检查函数没有隐式的返回值
noUnusedLocals检查未使用的局部变量
noUnusedParameters检查未使用的参数
allowUnreachableCode检查不可达代码;true:忽略不可达代码,false:不可达代码将引起错误
noEmitOnError有错误的情况下不进行编译,默认值:false

十、面向对象

要想面向对象,操作对象,首先便要拥有对象;

要创建对象,必须要先定义类,所谓的类可以理解为对象的模型;

程序中可以根据类创建指定类型的对象;

举例来说:

可以通过Person类来创建人的对象,通过Dog类创建狗的对象,不同的类可以用来创建不同的对象;

1. 定义类

// 使用class关键字来定义一个类
/*
*   对象中主要包含了两个部分:
*       属性
*       方法
*/

class Person {
    /*
*   直接定义的属性是实例属性,需要通过对象的实例去访问:
*       const per = new Person();
*       per.name
*
*   使用static开头的属性是静态属性(类属性),可以直接通过类去访问
*       Person.age
*
*   readonly开头的属性表示一个只读的属性无法修改
*/

    /*
        定义实例属性(new出实例之后才能访问的属性)
    */
    // name: string = '孙悟空';
    // age:number = 18;

    /*
        定义静态属性:在属性前使用static关键字可以定义类属性(静态属性)
    */
    // static age: number = 18;

    /*
        readonly开头的属性表示一个只读的属性无法修改
    */
    // readonly name: string = '孙悟空';

    name: string = '孙悟空';
    age:number = 18;

    /*
    * 定义方法:如果方法以static开头则方法就是类方法,可以直接通过类去调用
    */
    // 实例方法
    sayHello(){
        console.log('Hello 大家好!');
    }

    // 类方法
    static sayHello(){
        console.log('Hello 大家好!');
    }
}

/* 创建实例 */
//const per: Person = new Person(); 和下面相同
const per = new Person();

/* 属性 */

// 访问实例属性
console.log(per);
console.log(per.name, per.age);

// 访问静态(类)属性
// console.log(Person.age);

// 如果是readonly类型,则无法修改
// 下面的代码报错
// per.name = 'tom';

/* 方法 */

// 访问实例方法
per.sayHello();

// 访问静态方法
Person.sayHello();

2. 构造函数

可以使用constructor定义一个构造器方法;

class Dog {
    name: string;
    age: number;

    // constructor 被称为构造函数
    // 构造函数会在对象创建时调用
    // TS中仅能创建一个构造函数!
    constructor(name: string, age: number) {
        // 在实例方法中,this就表示当前的实例
        // 在构造函数中的当前对象就是新建的那个对象
        // 可以通过this向新建的对象中添加属性
        this.name = name;
        this.age = age;
    }

    bark() {
        // alert('汪汪汪!');
        // 在方法中可以通过this来表示当前调用方法的对象
        console.log(this.name);
    }
}

const dog = new Dog('小黑', 4);
const dog2 = new Dog('小白', 2);

console.log(dog);
console.log(dog2);

dog2.bark();

3. 继承

继承时面向对象中的又一个特性

通过继承可以将其他类中的属性和方法引入到当前类中

示例:

(function () {
    // 定义一个Animal类
    class Animal {
        name: string;
        age: number;

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

        sayHello() {
            console.log('动物在叫~');
        }
    }

    /*
    * Dog extends Animal
    *   - 此时,Animal被称为父类,Dog被称为子类
    *   - 使用继承后,子类将会拥有父类所有的方法和属性
    *   - 通过继承可以将多个类中共有的代码写在一个父类中,
    *       这样只需要写一次即可让所有的子类都同时拥有父类中的属性和方法
    *       如果希望在子类中添加一些父类中没有的属性或方法直接加就行
    *   - 如果在子类中添加了和父类相同的方法,则子类方法会覆盖掉父类的方法
    *       这种子类覆盖掉父类方法的形式,我们称为方法重写
    *
    */
    // 定义一个表示狗的类
    // 使Dog类继承Animal类
    class Dog extends Animal {
        run() {
            console.log(`${this.name}在跑~~~`);
        }

        sayHello() {
            console.log('汪汪汪汪!');
        }
    }

    // 定义一个表示猫的类
    // 使Cat类继承Animal类
    class Cat extends Animal {
        sayHello() {
            console.log('喵喵喵喵!');
        }
    }

    const dog = new Dog('旺财', 5);
    const cat = new Cat('咪咪', 3);
    console.log(dog);
    dog.sayHello();
    dog.run();
    console.log(cat);
    cat.sayHello();
})();

通过继承可以在不修改类的情况下完成对类的扩展

在子类中可以使用super来完成对父类的引用--调用父类的方法

(function () {
    class Animal {
        name: string;

        constructor(name: string) {
            this.name = name;
        }

        sayHello() {
            console.log('动物在叫~');
        }
    }

    class Dog extends Animal {
        age: number;

        // 如果在子类中写了构造函数,在子类构造函数中"必须"对父类的构造函数进行调用
        constructor(name: string, age: number) {
            // 调用父类的构造函数
            super(name);
            this.age = age;
        }

        sayHello() {
            // 在类的方法中 super就表示当前类的父类
            //super.sayHello();
            console.log('汪汪汪汪!');
        }
    }

    const dog = new Dog('旺财', 3);
    dog.sayHello();
})();

4. 抽象类(abstract class)

抽象类是专门用来被其他类所继承的类,它只能被其他类所继承不能用来创建实例

(function () {

    /*
    *   以abstract开头的类是抽象类,
    *       抽象类和其他类区别不大,只是不能用来创建对象
    *       抽象类就是专门用来被继承的类
    *
    *       抽象类中可以添加抽象方法
    */
    abstract class Animal {
        name: string;

        constructor(name: string) {
            this.name = name;
        }

        // 定义一个抽象方法
        // 抽象方法使用 abstract开头,没有方法体
        // 抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写
        abstract sayHello(): void;
    }

    // 下面的代码会报错
    // const an = new Animal();

    class Dog extends Animal {
        sayHello() {
            console.log('汪汪汪汪!');
        }
    }

    class Cat extends Animal {
        sayHello() {
            console.log('喵喵喵喵!');
        }
    }

    const dog = new Dog('旺财');
    dog.sayHello();
})();

使用abstract开头的方法叫做抽象方法,抽象方法没有方法体只能定义在抽象类中,继承抽象类时抽象方法必须要实现;

5. 接口

在接口中所有的方法都是抽象方法,而抽象类中可以有真实方法也可以有抽象方法

接口中的所有的属性都不能有实际的值; 接口只定义对象的结构,而不考虑实际;

(function () {
  //使用类型别名 描述一个对象的类型,不能重复声明
  type myType = {
    name: string,
    age: number,
  };

  /*
   *   接口用来定义一个类结构,用来定义一个类中应该包含哪些属性和方法
   *       同时接口也可以当成类型声明去使用
   */
  interface myInterface {
    name: string;
    age: number;
  }

  interface myInterface {
    gender: string;
  }
  // 接口可以重复声明,最终以所有声明的和为准
  const obj: myInterface = {
    name: "sss",
    age: 111,
    gender: "男",
  };

  /*
   * 接口可以在定义类的时候去限制类的结构,
   */
  interface myInter {
    name: string;

    sayHello(): void;
  }

  /*
   * 定义类时,可以使类去实现一个接口,
   *   实现接口就是使类满足接口的要求
   */
  class MyClass implements myInter {
    name: string;

    constructor(name: string) {
      this.name = name;
    }

    sayHello() {
      console.log("大家好~~");
    }
  }
})();

完整案例:

// interface 接口
interface PersonInterface {
  name: string;
  age: number; // :号 必须要写的
  sex?: string; // ?: 可选的
  readonly salary: number; // 只读 不能修改
  [propName: string]: any; //可添加任意类型属性,任意个数的属性
  greet(): void; //无返回值的函数
}

interface StudentInterface {
  id: number;
  course: string;
}

// 接口: class中
class People implements PersonInterface, StudentInterface {
  name = "米斯特吴";
  age: number = 31;
  salary: number = 8000;
  id: number = 101;
  course: string = "it";
  greet() {
    console.log("hello world");
  }
}
// interface 可以继承  type不能继承
// interface接口的继承
interface Employee extends PersonInterface {
  work: string;
}

const employee: Employee = {
  name: "米斯特吴",
  age: 32,
  salary: 7000,
  sex: "男",
  work: "前端开发",
  greet() {
    console.log("hello");
  },
};

console.log(employee);

6. 封装

对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装

默认情况下,对象的属性是可以任意的修改的,为了确保数据的安全性,在TS中可以对属性的权限进行设置

  • 静态属性(static):

    • 声明为static的属性或方法不再属于实例,而是属于类的属性;
  • 只读属性(readonly):

    • 如果在声明属性时添加一个readonly,则属性便成了只读属性无法修改
  • TS中属性具有三种修饰符:

    • public(默认值:公共的),可以在类、子类和对象中修改
    • protected(受保护的) ,可以在类、子类中修改
    • private (私有的),可以在类中修改

示例:

public:

class Person{
    public name: string; // 写或什么都不写都是public
    public age: number;

    constructor(name: string, age: number){
        this.name = name; // 可以在类中修改
        this.age = age;
    }

    sayHello(){
        console.log(`大家好,我是${this.name}`);
    }
}

class Employee extends Person{
    constructor(name: string, age: number){
        super(name, age);
        this.name = name; //子类中可以修改
    }
}

const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 可以通过对象修改

protected:

class Person{
    protected name: string;
    protected age: number;

    constructor(name: string, age: number){
        this.name = name; // 可以在类中修改
        this.age = age;
    }

    sayHello(){
        console.log(`大家好,我是${this.name}`);
    }
}

class Employee extends Person{

    constructor(name: string, age: number){
        super(name, age);
        this.name = name; //子类中可以修改
    }
}

const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 对象中不能修改

private:

class Person{
    private name: string;
    private age: number;

    constructor(name: string, age: number){
        this.name = name; // 可以在类中修改
        this.age = age;
    }

    sayHello(){
        console.log(`大家好,我是${this.name}`);
    }
}

class Employee extends Person{

    constructor(name: string, age: number){
        super(name, age);
        this.name = name; //子类中不能修改
    }
}

const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 对象中不能修改

7. 属性存取器

对于一些不希望被任意修改的属性,可以将其设置为private

直接将其设置为private将导致无法再通过对象修改其中的属性

我们可以在类中定义一组读取、设置属性的方法,这种对属性读取或设置的属性被称为属性的存取器

读取属性的方法叫做setter方法,设置属性的方法叫做getter方法

示例:

class Person{
     //私有属性前面一般加_
    private _name: string;

    constructor(name: string){
        this._name = name;
    }

    get name(){
        return this._name;
    }

    set name(name: string){
        this._name = name;
    }

}

const p1 = new Person('孙悟空');
// 实际通过调用getter方法读取name属性
console.log(p1.name);
// 实际通过调用setter方法修改name属性 
p1.name = '猪八戒'; 
(function () {
    // 定义一个表示人的类
    class Person {
        // TS可以在属性前添加属性的修饰符
        /*
        *   public 修饰的属性可以在任意位置访问(修改)默认值
        *   private 私有属性,私有属性只能在类内部进行访问(修改)
        *       - 通过在类中添加方法使得私有属性可以被外部访问
        *   protected 受保护的属性,只能在当前类和当前类的子类中访问(修改)
        *
        */
        private _name: string;
        private _age: number;

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

        /*
        *   getter方法用来读取属性
        *   setter方法用来设置属性
        *       - 它们被称为属性的存取器
        */

        // 定义方法,用来获取name属性
        // getName(){
        //     return this._name;
        // }

        // 定义方法,用来设置name属性
        // setName(value: string){
        //     this._name = value;
        // }

        // getAge(){
        //     return this._age;
        // }

        // setAge(value: number){
        //     // 判断年龄是否合法
        //     if(value >= 0){
        //         this._age = value;
        //     }
        // }

        // TS中设置getter方法的方式
        // 此时再使用per.name时,实际上是调用了get name()方法!
        get name() {
            console.log('get name()执行了!!');
            return this._name;
        }
        // TS中设置setter方法的方式
        // 此时再使用per.name = xxx时,实际上是调用了set name()方法!
        set name(value) {
            console.log('set name()执行了!!');
            this._name = value;
        }

        get age() {
            return this._age;
        }

        set age(value) {
            if (value >= 0) {
                this._age = value
            }
        }
    }

    const per = new Person('孙悟空', 18);

    /*
    * 现在属性是在对象中设置的,属性可以任意的被修改,
    *   属性可以任意被修改将会导致对象中的数据变得非常不安全
    */

    // per.setName('猪八戒');
    // per.setAge(-33);

    per.name = '猪八戒';
    per.age = -33;

    console.log(per);

    /*
     * protected演示
     */
    class A {
        protected num: number;

        constructor(num: number) {
            this.num = num;
        }
    }

    class B extends A {
        test() {
            console.log(this.num);
        }
    }

    const b = new B(123);
    // b.num = 33; 不能通过对象修改

8. 命名空间

// namespace: 帮助我们隔离环境变量的污染,注意要用export导出
namespace MyMath {
  export const PI = 3.14;

  export function sumValue(num1: number, num2: number): number {
    return num1 + num2;
  }

  export function calcCircle(value: number) {
    return value * PI;
  }
}

const PI = 2.88;

console.log(MyMath.sumValue(15, 10));
console.log(PI); //2.88
console.log(MyMath.PI); //3.14

9. 泛型

/*
function fn(a: any): any{
    return a;
}
*/

/*
 * 在定义函数或是类时,如果遇到类型不明确就可以使用泛型
 <T>是在定义一个泛型T
 */
function fn<T>(a: T): T {
    return a;
}

/* I.可以直接调用具有泛型的函数 */
// 1.不指定泛型,TS可以自动对类型进行推断
let result = fn(10);
// 2.指定泛型
let result2 = fn<string>('hello');

/* II.泛型可以同时指定多个 */
function fn2<T, K>(a: T, b: K): T {
    console.log(b);
    return a;
}

fn2<number, string>(123, 'hello');

// T[]
function map<T>(params: Array<T>) {
  return params;
}
map<string>(['123']);

/* III.限制泛型范围 */
interface Inter {
    length: number;
}

// T extends Inter 表示泛型T必须时Inter实现类(子类)
function fn3<T extends Inter>(a: T): number {
    return a.length;
}

fn3({length: 10});

/* Ⅳ.类中使用泛型 */
class MyClass<T> {
    name: T;

    constructor(name: T) {
        this.name = name;
    }
}

const mc = new MyClass<string>('孙悟空');

//数组泛型
let last = <T>(arr: Array<T>) => {
  return arr[arr.length - 1]
}
let last = <T>(arr: T[]) => {
  return arr[arr.length - 1]
}
  const l1 = last<number>([1,2,3,4])
  const l1 = last<string>(["a","b"])

十一、附录

1. 注解

注1:ts类型声明时,被声明的变量会拥有该类型的属性/方法

interface Point {
  x: number;
  y: number;
}

const point: Point = {
  x: 3,
  y: 4
};
point.x //被Point声明后point会拥有x,y属性

const sum: number = 1
sum.toString //被number声明后sum会拥有number的方法

注2:JSON的类型声明

// 其他的 case
interface Person {
  name: 'string';
}
const rawData = '{"name": "dell"}';
const newData: Person = JSON.parse(rawData);

注3:解构的类型声明

function App() {
  function add({ first, second }: { first: number; second: number }): number {
    return first + second;
  }

  function getNumber({ first }: { first: number }) {
    return first;
  }

  const obj = { first: 4, second: 5 };

  const total = add(obj);

  const count = getNumber({ first: 1 });

注4:Interface的使用

// interface 和 type 相类似,但并不完全一致
interface Person {
  // readonly name: string;
  name: string;
  age?: number;
  [propName: string]: any;
  say(): string;
  // say: () => string;
}
//接口继承
interface Teacher extends Person {
  teach(): string;
}

//注意person中定义的参数可以多,但不能少
const person = {
  name: 'dell',
  sex: 'male',
  say() {
    return 'say hello';
  },
  teach() {
    return 'teach';
  }
};
const getPersonName = (person: Person): void => {
  console.log(person.name);
};

const setPersonName = (person: Teacher, name: string): void => {
  person.name = name;
};

getPersonName(person);
setPersonName(person, 'lee');

// 声明一个类,实现Person接口
class User implements Person {
  name = 'dell';
  say() {
    return 'hello';
  }
}
// interface定义函数类型的接口
interface SayHi {
  (word: string): string;
}

const say: SayHi = (word: string) => {
  return word;
};

注5:类型守卫:解决联合类型使用起来的问题--类型保护

interface Bird {
    fly: boolean
    sing: () => void
}
interface Dog {
    fly: boolean
    bark: () => {}
}
const bird: Bird = {
    fly: true,
    sing() {
        console.log(111);
    }
}
const dog: Dog = {
    fly: false,
    bark: function () {
        return {
            a: 1
        }
    }
}
// 类型断言方法来做类型保护
function trainAnimal(animal: Bird | Dog) {
    if (animal.fly) {
        (animal as Bird).sing()
    } else {
        (animal as Dog).bark()
    }
}
// in 语法来做类型保护
function trainAnimal2(animal: Bird | Dog) {
    if ("sing" in animal) {
        animal.sing()
    } else {
        animal.bark()
    }
}

// typeof语法来做类型保护
function add(first: number | string, second: number | string) {
    if (typeof first === 'string' || typeof second === 'string') {
        return `${first}${second}`
    } else {
        return first + second
    }
}
// interface 语法来做类型保护
class Num {
    count: number
}
function add2(first: object | Num, second: object | Num ) {
    if(first instanceof Num && second instanceof Num){
        return first.count + second.count
    }
    return 0
}

注6:构造器简写(public,protected,private同理):

  /*
     * 可以直接将属性定义在构造函数中
     *
     * 下面两个构造方法效果是一样的!
     */
    /*
        class C{
         name: string;
         age: number

         constructor(name: string, age: number) {
            this.name = name;
            this.age = age;
         }
     }
     等价于下面
     */
   //简写写法
    class C {
        // 可以直接将属性定义在构造函数中
        constructor(public name: string, public age: number) {
        //可以省略上面类中上方的属性声明及下方的赋值语句
        }
    }

    const c = new C('xxx', 111);

    console.log(c);
})();

注7:构造器简写继承写法:

//传统写法
  class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  sayHello() {
    console.log('动物在叫~');
  }
}

class Dog extends Animal {
  age: number;

  // 如果在子类中写了构造函数,在子类构造函数中"必须"对父类的构造函数进行调用
  constructor(name: string, age: number) {
    // 调用父类的构造函数
    super(name);
    this.age = age;
  }

  sayHello() {
    // 在类的方法中 super就表示当前类的父类
    //super.sayHello();
    console.log('汪汪汪汪!');
  }
}

const dog = new Dog('旺财', 3);
dog.sayHello();

//简写
class Animal {
    // constructor在 new一个实例时执行
    constructor(public name: string) {
    }

    sayHello() {
        console.log('动物在叫~');
        this.name
    }
}

class Dog extends Animal {

    // 如果在子类中写了构造函数,在子类构造函数中"必须"对父类的构造函数进行调用
    constructor(public name: string, public age: number) {
        // 调用父类的构造函数
        super(name);

    }

    sayHello() {
        // 在类的方法中 super就表示当前类的父类
        //super.sayHello();
        console.log('汪汪汪汪!');

    }
}
const animal = new Animal('小白');

const dog = new Dog('旺财', 3);
dog.sayHello();
console.log(dog.name);
console.log(dog.age);
console.log(animal.name);

注8:TS实现设计模式--单例模式

// 单例模式--就是在整个程序中有且仅有一个实例。
class Demo {
  // 创建一个私有的静态属性 instance,初始值为undefined
  private static instance: Demo;
  // 创建constructor构造器并声明name属性
  private constructor(public name: string) {}
  // 静态方法--如果instance为空,则创建实例,否则返回instance
  static getInstance() {
    if (!this.instance) {
      this.instance = new Demo("dell lee");
    }
    return this.instance;
  }
}
// 下方两个语句创建的实例是同一个
const demo1 = Demo.getInstance();
const demo2 = Demo.getInstance();
console.log(demo1.name);
console.log(demo2.name);

注9:interface接口的继承

interface Person {
  name: string;
}

interface Teacher extends Person {
  teachingAge: number;
}

interface Student extends Person {
  age: number;
}

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

const teacher = {
  name: 'dell',
  teachingAge: 3
};

const student = {
  name: 'lee',
  age: 18
};
//参数的类型声明可以比你传入的参数要少,但必须包含你使用的参数
const getUserInfo = (user: Person) => {
  console.log(user.name);
};

getUserInfo(teacher);
getUserInfo(student);

注10:泛型的使用

class DataManager<T extends number | string> {
  constructor(private data: T[]) { }
  getItem(index: number): T {
    return this.data[index];
  }
}

const data = new DataManager<string>(['1']);
const data1 = new DataManager<number>([1]);
data.getItem(0);


interface Item {
  name: string;
}

//泛型必须拥有item里面所有的东西
class DataManager1<T extends Item> {
  constructor(private data: T[]) { }
  //T[]这样表示数组中每一项都是T类型
  getItem(index: number): T {
    return this.data[index];
  }
}

const data3 = new DataManager1<Item>([{name: 'dell'}]);

const data4 = new DataManager<number>([]);

// 如何使用泛型作为一个具体的类型注解
function hello<T>(params: T) {
  return params;
}

const func: <T>(param: T) => T = hello;

注11:命名空间的使用

components.ts文件

namespace Components {

   //可以在命名空间中写接口导出
  export interface user {
    name: string;
  }

  export class Header {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'This is Header';
      document.body.appendChild(elem);
    }
  }

  export class Content {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'This is Content';
      document.body.appendChild(elem);
    }
  }

  export class Footer {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'This is Footer';
      document.body.appendChild(elem);
    }
  }
}

page.ts文件

// 表示命名空间的引用关系,前面用///斜杠表示
///<reference path="components.ts" />

namespace Home {

     //命名空间中可以嵌套命名空间
  export namespace Dell {
    export const teacher: Components.user = {
      name: 'dell'
    };
  }
  export class Page {
    constructor() {
      new Components.Header();
      new Components.Content();
      new Components.Footer();
      new Components.Footer();
    }
  }
}

2. TS练习题

参考文章