2-快速理解TypeScript类型系统

1. 重点提炼

  • 类型标注语法
    • 类型检测的好处
    • 使用场景
    • 常用的类型标注的使用
  • 基础类型标注
    • 字符串、数字、布尔值、空、未定义
  • 非基础类型标注
    • 对象、数组
  • 特殊类型
    • 元组、枚举、无值类型、Never类型、任意类型、未知类型
  • 函数基本标注

2. 什么是类型

程序 = 数据结构 + 算法 = 各种格式的数据 + 处理数据的逻辑

2.1 数据是有格式(类型)的

  • 数字、布尔值、字符
  • 数组、集合

2.2 程序是可能有错误的

  • 计算错误(对非数字类型数据进行一些数学运算)
  • 调用一个不存在的方法

不同类型的数据有不同的操作方式或方法,如:字符串类型的数据就不应该直接参与数学运算

如何避免以上错误呢?

一般写js,会人为排查,设置打断点,控制台等。

2.3 动态类型语言 & 静态类型语言

动态类型语言

程序运行期间才做数据类型检查的语言,如:JavaScript

静态类型语言

程序编译期间做数据类型检查的语言,如:Java

2.4 静态类型语言的优缺点

优点

  • 程序编译阶段(配合IDE、编辑器甚至可以在编码阶段)即可发现一些潜在错误,避免程序在生产环境运行了以后再出现错误
  • 编码规范、有利于团队开发协作、也更有利于大型项目开发、项目重构
  • 配合IDE、编辑器提供更强大的代码智能提示/检查
  • 代码即文档

缺点

  • 麻烦
  • 缺少灵活性

2.5 动态类型语言的优缺点

优点

  • 静态类型语言的缺点(方便、灵活)

缺点

  • 静态类型语言的优点

静态类型语言的核心 : 类型系统——无论是检测错误、还是类型检查、编码规范、或者配合IDE进行排错

3. 什么是类型系统

类型系统包含两个重要组成部分

  • 类型标注(定义、注解) - typing
  • 类型检测(检查) - type-checking

3.1 类型标注

类型标注就是在代码中给数据(变量、函数(参数、返回值))添加类型说明,当一个变量或者函数(参数)等被标注以后就不能存储或传入与标注类型不符合的类型

如果给一个函数参数标注string类型,它就不能接受其他类型的参数了。

有了标注,TypeScript 编译器就能按照标注对这些数据进行类型合法检测。

有了标注,各种编辑器、IDE等就能进行智能提示

这样就能在编码阶段帮助我们排查很多错误,避免运行出错。

3.2 类型检测

顾名思义,就是对数据的类型进行检测。

注意这里,重点是类型两字。

类型系统检测的是类型,不是具体值(虽然,某些时候也可以检测值),比如某个参数的取值范围(1-100之间),我们不能依靠类型系统来完成这个检测,它应该是我们的业务层具体逻辑,类型系统检测的是它的值类型是否为数字!

通过在代码中给变量、函数的参数、返回值标注类型,在代码的编译阶段,通过IDE就能帮助我们检测出来类型是否有误。

4. 类型标注

TypeScript 中,类型标注的基本语法格式为:

数据载体 => 变量、函数的参数、返回值等

数据载体:类型

TypeScript 的类型标注,我们可以分为

  • 基础的简单的类型标注
  • 高级的深入的类型标注

4.1 基础的简单的类型标注

  • 基础类型
  • 空和未定义类型
  • 对象类型
  • 数组类型
  • 元组类型
  • 枚举类型
  • 无值类型
  • Never类型
  • 任意类型
  • 未知类型(Version3.0 Added)

4.1.1 基础类型

基础类型包含:stringnumberboolean

标注语法

let title: string = 'CSDN';
let n: number = 100;
let isOk: boolean = true;

配合支持ts的编辑器,它就会根据类型检测系统,帮助我们把错误排查出来。

在这里插入图片描述

tsc

用tsc编译也会报错。

在这里插入图片描述

4.1.2 空和未定义类型

因为在 NullUndefined 这两种类型有且只有一个值,在标注一个变量为 NullUndefined 类型,那就表示该变量不能修改了

let a: null;
// ok
a = null;
// error
a = 1;

如:

let a: null;
let b: undefined;

a = 1;

在这里插入图片描述

默认情况下 nullundefined 是所有类型的子类型。 就是说你可以把 nullundefined 赋给其它类型的变量。

let a: number;
// ok
a = null;

如果一个变量声明了,但是未赋值,那么该变量的值为 undefined,但是如果它同时也没有标注类型的话,默认类型为 anyany 类型后面有详细说明

// 类型为 `number`,值为 `undefined`
let a: number;
// 类型为 `any`,值为 `undefined`
let a: any;

小技巧

因为 nullundefined 都是其它类型的子类型,所以默认情况下会有一些隐藏的问题。

类型检测只检测类型,以及类型下面的方法是否符合类型要求,但不会检测具体的值。

因此检测不出问题,但是运行就会报错,因为nullundefined 下没有toFixed方法。

let a:number;
a = null;
// ok(实际运行是有问题的)
a.toFixed(1);

小技巧:指定 strictNullChecks 配置为 true,可以有效的检测 null 或者 undefined,避免很多常见问题 => 严格的空值检查

let a:number;
a = null;
// error
a.toFixed(1);

也可以使我们程序编写更加严谨

如果不设置严格的空值检查(指定 strictNullChecks 配置为 true

let ele = document.querySelector('div');
ele.style.display = 'none';

以上写法不会有任何问题,但是实际运行中可能报错,因为在页面中可能没有该元素,dom操作返回null,然后必然报错。

因此运用ts,最好设置严格的空值检查。

在这里插入图片描述

我们加一个判断,就不报错了。

let ele = document.querySelector('div');
// 获取元素的方法返回的类型可能会包含 null,所以最好是先进行必要的判断,再进行操作
if (ele) {
    
    
		ele.style.display = 'none';
}

4.1.3 对象类型

4.1.3.1 内置对象类型

JavaScript 中,有许多的内置对象,比如:Object、Array、Date……,我们可以通过对象的 构造函数 或者 来进行标注

let a: object = {
    
    };  // 对象类型
// 数组这里标注格式有点不太一样,后面我们在数组标注中进行详细说明
let arr: Array<number> = [1,2,3];   // 数字型数组类型
let d1: Date = new Date();  // 日期类型

4.1.3.2 自定义对象类型

另外一种情况,许多时候,我们可能需要自定义结构的对象。这个时候,我们可以:

  • 字面量标注
  • 接口
  • 定义 或者 构造函数

这种写法是有问题的,因为类型标注实际上能够限定和约定数据的存储格式,如果这样标注,往里写任何东西都不会报错的,虽然看起来没问题的,但是我们肯定希望能够确定出那些数据是可以存的,哪些是不能存的。

let a: object = {
    
    
    x: 1,
    y: 2
};

如何通过类型约束来约束对象 =>

4.1.3.2.1 字面量标注
let a: {
    
    username: string; age: number} = {
    
    
  username: 'ls',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;

优点 : 方便、直接

缺点 : 不利于复用和维护,因此用接口解决这种问题

为了优化可复用性,增加了接口

4.1.3.2.2 接口
// 这里使用了 interface 关键字,在后面的接口blog中会详细说
interface Person {
    
    
  username: string;
  age: number;
};
let a: Person = {
    
    
  username: 'ls',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;

// 再定一个对象
let user1: Person = {
    
    
    username: 'zs',
    age: 30
}

// error 接口只算一种数据类型,不是实体变量
let user2 = Person;
4.1.3.2.2.1 实例
interface Person {
    
    
    username: string;
    age: number;
}

let user: Person = {
    
    
    username: 'ls',
    age: 35
}
let user1: Person = {
    
    
    username: 'zs',
    age: 30
}

tsc

对应的js文件

=> 我们发现interface实际没有的,它只存在于源码和编译阶段,在实际编译过后的代码中是不存在的。

var user = {
    
    
    username: 'ls',
    age: 35
};
var user1 = {
    
    
    username: 'zs',
    age: 30
};
4.1.3.2.2.2 优缺点

优点 : 复用性高

缺点 : 接口只能作为类型标注使用,不能作为具体值,它只是一种抽象的结构定义,并不是实体,没有具体功能实现

有的时候希望通过一种类型定义一种结构,同时又利用它定义不同的对象

4.1.3.2.3 类与构造函数
// 类的具体使用,也会在后面的blog中说明
class Person {
    
    
	constructor(public username: string, public age: number) {
    
    
  }
}
// 前一个Person是类型,后一个Person是构造函数
let a: Person = new Person('ls', 35);
// ok
a.username;
a.age;
// error
a.gender;
4.1.3.2.3.1 实例
class Person {
    
    
    
    constructor(public username: string, public age: number) {
    
    

    }
}

let user: Person = new Person('ls', 35);

tsc

编译出来的代码是包含构造函数的部分的。

var Person = /** @class */ (function () {
    
    
    function Person(username, age) {
    
    
        this.username = username;
        this.age = age;
    }
    return Person;
}());
var user = new Person('ls', 35);
4.1.3.2.3.2 优缺点

优点 : 功能相对强大,定义实体的同时也定义了对应的类型

缺点 : 复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单

使用接口可以定义函数类型

interface AjaxOptions {
    
    
    url: string;
    method: string;
}

function ajax(options: AjaxOptions) {
    
    }

ajax({
    
    
    url: '',
    method: 'get'
});

同时编译器,还会提示参数类型。

在这里插入图片描述

在这里插入图片描述

4.1.3.3 扩展

4.1.3.3.1 包装对象

这里说的包装对象其实就是 JavaScript 中的 StringNumberBoolean,我们知道 string 类型(字符串类型) 和 String 类型(包装对象 => 字符串对象)并不一样,在 TypeScript 中也是一样

let a: string;
a = '1';
// error 
// String有的,string不一定有(包装对象有的,基础类型不一定有,但反过来是一定的)
a = new String('1');

let b: String;
b = new String('2');
// ok 
// 和上面正好相反
b = '2';

将字符串对象赋给字符串是必然损失很多东西的,但是将字符串赋给字符串对象是不会有损失的,因为字符串对象是可扩展的,普通类型的字符串是不可扩展的。(类似高级语言中的强制类型转换 => 导致内容丢失)

4.1.4 数组类型

TypeScript 中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型

4.1.4.1 使用泛型标注

// <number> 表示数组中存储的数据类型,泛型具体概念后续会讲
let arr1: Array<number> = [1,2,3];
// ok
arr1.push(100);
// error
arr1.push('csdn');

4.1.4.2 简单标注

let arr2: string[] = ['a', 'b', 'c'];
// ok
arr2.push('csdn');
// error
arr2.push(1);

如果想往数组中添加不一样的数据 => 元组

4.1.5 元组类型

元组类似数组,但是存储的元素类型不必相同,实际就是联合类型数据,只要满足其一即可完成添加,但是需要注意:

  • 初始化数据的个数以及对应位置标注类型必须一致
    在这里插入图片描述

在这里插入图片描述

  • 越界数据必须是元组标注中的类型之一(标注越界数据可以不用对应顺序 - 联合类型
let data1: [string, number] = ['csdn', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);

4.1.6 枚举类型

枚举的作用组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字(如果学过CC++类似高级语言,这个更不陌生了)

一般枚举名称习惯全大写(遵循定义之后就不能修改了,类似常量,这点也和其他高级语言的约束一致)

enum HTTP_CODE {
    
    
  OK = 200,
  NOT_FOUND = 404,
  METHOD_NOT_ALLOWED        
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;

注意事项:

  • key 不能是数字(如上定义的OKNOT_FOUNDMETHOD_NOT_ALLOWED

  • value 可以是数字,称为 数字类型枚举,也可以是字符串,称为 字符串类型枚举,但不能是其它值,默认为数字:0

  • 枚举值可以省略,如果省略,则:

    • 第一个枚举值默认为:0
    • 非第一个枚举值为上一个数字枚举值+ 1
  • 枚举值为只读(常量),初始化后不可修改
    在这里插入图片描述

4.1.6.1 字符串类型枚举

枚举类型的值,也可以是字符串类型

enum URLS  {
    
    
  USER_REGISETER = '/user/register',
  USER_LOGIN = '/user/login',
  // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
  INDEX = 0
}

注意:如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值

小技巧:枚举名称可以是大写,也可以是小写,推荐使用全大写(通常使用全大写的命名方式来标注值为常量)

4.1.6.2 实例

enum HTTP_CODE {
    
    
    OK = 200,
    NOT_FOUND = 404,
    METHOD_NOT_ALLOWED
};

enum URLS  {
    
    
    USER_REGISETER = '/user/register',
    USER_LOGIN = '/user/login',
    // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
    INDEX = 1
}
URLS.USER_LOGIN;

编译后

tsc

HTTP_CODE会被编译成一个对象,在对象里边,是一种的嵌套关系。

var HTTP_CODE;
(function (HTTP_CODE) {
    
    
    HTTP_CODE[HTTP_CODE["OK"] = 200] = "OK";
    HTTP_CODE[HTTP_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
    HTTP_CODE[HTTP_CODE["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
})(HTTP_CODE || (HTTP_CODE = {
    
    }));

var URLS;
(function (URLS) {
    
    
    URLS["USER_REGISETER"] = "/user/register";
    URLS["USER_LOGIN"] = "/user/login";
    // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
    URLS[URLS["INDEX"] = 1] = "INDEX";
})(URLS || (URLS = {
    
    }));
URLS.USER_LOGIN;

如 : 首先给HTTP_CODE增加一个OK属性,它的值是200。然后再用200又作为key设置给HTTP_CODE,它的值为OK

HTTP_CODE[HTTP_CODE["OK"] = 200] = "OK";

=>

HTTP_CODE['OK'] = 200;

HTTP_CODE['200'] = 'OK';

=>

既能通OK访问到200,又能通过200访问到OK,可以理解为自枚举

4.1.7 无值类型

表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为:void

function fn():void {
    
    
  	// 没有 return 或者 return undefined
}

strictNullChecksfalse 的情况下,undefinednull 都可以赋值给 void ,但是当 strictNullCheckstrue 的情况下,只有 undefined 才可以赋值给 void

4.1.8 Never类型

当一个函数永远不可能执行 return 的时候,返回的就是 never ,与 void 不同,void 是执行了 return, 只是没有值,never 是不会执行 return,比如抛出错误,导致函数终止执行

不是太常用

function fn(): never {
    
    
  	throw new Error('error');
}

4.1.9 任意类型

有的时候,我们并不确定这个值到底是什么类型或者不需要对该值进行类型检测,就可以标注为 any 类型

let a: any;
  • 一个变量申明未赋值且未标注类型的情况下,默认为 any 类型
  • 任何类型值都可以赋值给 any 类型
  • any 类型也可以赋值给任意类型
  • any 类型有任意属性和方法
let a: any;
a = 1;
let b: number;
b = a;

注意:标注为 any 类型,也意味着放弃对该值的类型检测,同时放弃IDE 的智能提示

有时会产生隐含的 any 类型

function fn(a) {
    
    
    a.indexOf('a', 1);
}

这样写(万一忘记写a: string)是不会报错的。
在这里插入图片描述

小技巧:当指定 noImplicitAny 配置为 true,当函数参数出现隐含的 any 类型时报错

4.1.10 未知类型

unknow,3.0 版本中新增,属于安全版的 any,但是与 any 不同的是:

  • unknow 仅能赋值给 unknowany
  • unknow 没有任何属性和方法

这样实际会报错,但是检测不出来。

let c: any = 'csdn';

let d: number = 1;

d.toFixed(1);

d = c;
// NumberObject.toFixed(num)
d.toFixed(1);

unknow 仅能赋值给 unknowany

let c: unknown = 'csdn';

let d: number = 1;

d.toFixed(1);

d = c;
d.toFixed(1);

在这里插入图片描述

unknow 没有任何属性和方法

//ok
let c: any;
c.a;
// error
let d: unknown;
d.a;

在这里插入图片描述

4.1.11 函数类型

JavaScript 函数是非常重要的,在 TypeScript 也是如此。同样的,函数也有自己的类型标注格式

  • 参数
  • 返回值
函数名称( 参数1: 类型, 参数2: 类型... ): 返回值类型;
function fn1(x: number, y: number): number {
    
    
    return x + y;
}

let v: number = fn1(1, 2);

在这里插入图片描述

参数如果填错了,也会检测出来的。

在这里插入图片描述

函数更多的细节内容,在后期有专门的blog来进行深入的探讨



(后续待补充)

猜你喜欢

转载自blog.csdn.net/u013946061/article/details/107884215