构建Typescript知识体系(一)-TS及基础类型浅析

611 阅读10分钟

这是我参与更文挑战的第七天,活动详情查看:更文挑战

从移动终端到后端服务,从 IOT 到神经网络,JavaScript 几乎无处不在。 如此广阔的应用领域,自然对语言的安全性,健壮性,和可维护性有更高的要求。尽管,ECMAScript 标准在近几年有长足的进步,但在类型检查方面依然无所建树。 你是否经常遇到这样的场景

场景一: 你调用一个别人写的函数,但是别人没有留下任何注释,为了搞清楚参数类型,只能硬着头皮去看里面的逻辑

场景二: 为了保证代码的健壮性,你对一个函数的输入参数进行了各种假设(写了 N+1 个参数类型判断)

场景三:领导很看好你,让你维护一个重要的底层类库,你殚精竭虑,优化了一个参数类型,但是不知道有多少处引用,在提交代码前,是否感到如履薄冰呢?

场景四: 明明定义好了接口,可一联调就报错了,于是你生气的找后端理论:"这个字段是字符串!那个字段是数组!还有那个字段是数字!!!....."

以上情况,归根结底,是因为 JavaScript 是一门动态弱类型语言,对变量的类型非常宽容, 而且不会在变量与调用者之间建立结构化契约。如果你长期在没有类型约束的环境下开发,就会造成类型思维的缺失,养成不良的编程习惯!

Typescript 是什么

拥有类型系统的 JavaScript 超集,可以编译成纯 JavaScript,(为 JavaScript 提供静态类型检查)

Typescript 的特点

类型检查

TypeScript 会在编译代码时,进行严格的静态类型检查。意味着可以**在编码阶段发现存在的隐患,而不用把他们带到线上去

语言扩展

TypeScript 会包括 来自 ES6 和未来提案中的特性,比如异步操作和装饰器,也会从其他语言借鉴特性,比如接口和抽象类

工具属性

TypeScript 可以编译成 JavaScript,在任何浏览器,操作系统上运行。无需任何运行时的额外开销

为什么要使用 TypeScript

VSCODE 具备强大的 自动补全,导航,重构功能。使接口定义可以直接代替文档,同时可以提高开发效率,降低维护成本

TypeScript 可以帮助团队重塑"类型思维",接口的提供方将被迫去思考 API 的边界,他们将从代码的编写者蜕变为代码的设计者。

如果 JavaScript 是一匹野马,TypeScript 就是束缚野马的缰绳,作为"骑士"的你,自然可以张开双臂,放飞自我。但是,如果不是技艺超群,恐怕会摔的很掺。然而如果抓住了缰绳,你即可闲庭信步,亦可策马扬鞭,这就是 TypeScript 的价值,它可以让你在前端开发之路上走得更稳,更远。

学习 TypeScript 后,能让你在编程中形成"类型思维", 因为思维方式决定编程习惯、编程习惯奠定了工程质量,工程质量划定了能力边界。面对越来越复杂的前端应用,TypeScript 提供的思维方法,能够让你在未来的开发中长期受益!


强类型语言

解释一: 在 1974 年,一位美国科学家定义为:在强类型语言中,当一个对象从调用函数传递到被调用函数时,其类型必须与被调用的函数中声明的类型兼容

解释二: 不允许改变变量的数据类型,除非强制类型转换

弱类型语言

变量可以被赋予不同的数据类型

静态类型语言

编译阶段确定所有变量的类型

  1. 编译阶段确定偏移量
  2. 用偏移量访问代替属性名访问
  3. 偏移量信息共享

动态类型语言

执行阶段确定所有变量的类型

  1. 在程序运行时,动态计算属性偏移量
  2. 需要额外的空间存储属性名
  3. 所有对象的偏移量各存一份
静态类型语言动态类型语言
对类型极度严格对类型非常宽松
立即发现错误Bug 可能隐藏数月甚至数年
运行时性能好运行时性能差
自文档化可读性差

动态类型语言的支持者认为:

  • 性能是可以改善的(V8 引擎),而语言的灵活性更重要
  • 隐藏的错误可以通过单元测试发现
  • 文档可以通过工具生成

说明任何语言都具有两面性,同时也是在不断发展和进化的,不能一概而论,要看具体的场景和性价比。

image.png

编写第一个 typescript

初始化前端项目

新建"typescript-leanring" 目录,进入该目录,执行以下命令

// 第一步
npm init -y

// 第二步
npm i typescript -g

//第三步
npm tsc --init

// 第四步
npm i webpack webpack-cli webpack-dev-server --save-dev

新建src目录,进入改目录,创建 index.ts文件

let hello: string = "hello world";

基本类型

ES6 的数据类型typescript 的数据类型
BooleanBoolean
NumberNumber
StringString
ArrayArray
FunctionFunction
ObjectObject
SymbolSymbol
undefinedundefined
nullnull
void
any
元组
枚举
高级类型

类型注解(TS 中声明类型的方式)

格式 变量/函数:type

解释: 在变量或函数后加一个冒号, 冒号跟类型。变量的数据类型是一般不能改变的

原始类型

boolean , number  , string

let bool: boolean = true;
let num: number | null | undefined = 1;
let str: string = "1";

数组

1. 数字类型的数组

let ar1: number[] = [1, 2, 3];
let arr2: Array = [1, 2, 3];

2. 不同类型的数组   --联合类型

let arr3: Array<number | null | string | undefined> = [
  1,
  "2",
  3,
  "4",
  null,
  undefined,
];

3. 元组--一种特殊的数组,限制了数组的类型,顺序,个数. 建议: 只用来访问

let tuple: [number, string] = [1, "2"];

函数

let add = (x: number, y: number) => x + y;

在函数参数括号之后可以加上函数返回值的类型, 但通常返回值类型可以省略(因为使用了 ts 的类型推导功能)

let add1 = (x: number, y: number): number => x + y;

对象

let obj: object = { x: 1, y: "2" };
/*
obj.x= 6 出现警告
因为 在 ts中 obj只知道是object,并没有具体到某一个属性的类型
*/

let obj1: { x: number; y: string } = { x: 1, y: "2" };
obj1.x = 1;

symbol

let x1: symbol = Symbol();
let x2 = Symbol();

undefined null

let un: undefined = undefined;
let nu: null = null;

void

在 JavaScript 中: void 让任何表达式返回 undefined。如: void 0; 在 ts 中 表示 没有任何返回值得类型

let noReturn = () => {};

any   如过不设置任何类型默认就是 any

let x;

never   永远不会有返回值的类型

// 第一种情况,函数抛出异常
let error = () => {
  throw new Error("ERROR");
};
// 第二种情况 , 死循环
let endless = () => {
  while (true) {}
};

使用联合类型 声明变量的类型

let num: number | null | undefined = 1;
num = null;

一个角色判断的例子

function initByRole(role) {
  if (role === 1 || role === 2) {
    // do some
  } else if (role === 3 || role === 4) {
    // dom some
  } else if (role === 5) {
    //
  } else {
    //
  }
}
/*
一个系统有多种角色,
一个角色有很多种权限, 权限又对应不同的界面


问题:
1. 可读性差,很难记住数字的含义
2. 可维护性差,硬编码,牵一发动全身

如何解决:使用枚举
*/

枚举

枚举: 一组有名字的常量集合, 枚举成员的值定义后不能修改

数字枚举

第一个枚举成员的值默认是 0 ,往后递增

enum Role {
  Reporter,
  Developer,
  Owner,
  Guster,
}

可以设置枚举成员的默认值, 默认往后递增

enum Role1 {
  Reporter = 1,
  Developer,
  Owner,
  Guster,
}

console.log(Role1);
/*
{
1: "Reporter",
2: "Developer",
3: "Owner",
4: "Guster",
Developer: 2,
Guster: 4,
Owner: 3,
Reporter: 1,
}
*/

枚举成员实际上是一个对象

  1. 可以使用枚举成员的值进行访问
  2. 可以使用枚举成员的名字进行访问

编译后的 JavaScript 代码如下

var Role1;
(function (Role1) {
  Role1[(Role1["Reporter"] = 1)] = "Reporter";
  Role1[(Role1["Developer"] = 2)] = "Developer";
  Role1[(Role1["Owner"] = 3)] = "Owner";
  Role1[(Role1["Guster"] = 4)] = "Guster";
})(Role1 || (Role1 = {}));

枚举的实现原理: 反向映射 在编译成 JavaScript 对象后, 内层:枚举的成员的名称作为 key,值作为 value,返回 value(A) 外层: A 作为 key ,枚举的成员的名称作为 value

字符串枚举

enum Message {
  Success = "恭喜你,成功了",
  Fail = "抱歉",
}

编译成 JavaScript 代码后

var Message;
(function (Message) {
  Message["Success"] = "\u606D\u559C\u4F60,\u6210\u529F\u4E86";
  Message["Fail"] = "\u62B1\u6B49";
})(Message || (Message = {}));

/*
只有字符串的名称作为了key, 
因此字符串枚举是不能进行反向映射的
*/

异构枚举

把数字枚举与字符串枚举混用,就组成了异构枚举(不推荐使用)

enum Answer {
  N,
  Y = "yes",
}

枚举成员

分类

  1. 常量枚举(会在编译时计算出结果,然后以常量的形式出现在运行时环境)
    1. 没有初始值的情况
    2. 对已有枚举成员的引用
    3. 常量表达式
  2. 需要被计算的枚举(一些非"常量"的表达式,枚举成员的值不会在编译阶段进行计算,而会被保留到运行时环境进行计算)
enum Char {
  // 没有初始值的情况
  a,
  // 对已有枚举成员的引用
  b = Char.a,
  // 常量表达式
  c = 1 + 3,

  // 需要被计算的枚举
  d = Math.random(),
  e = "123".length,
  // 需要被计算的枚举一定需要被赋值,否则会有警告
  // f= 4
}

编译成 JavaScript 代码后

var Char;
(function (Char) {
  Char[(Char["a"] = 0)] = "a";
  Char[(Char["b"] = 0)] = "b";
  Char[(Char["c"] = 4)] = "c";
  Char[(Char["d"] = Math.random())] = "d";
  Char[(Char["e"] = "123".length)] = "e";
})(Char || (Char = {}));

/*
常量枚举成员已经被计算出了结果

需要被计算的枚举成员的值被保留了,在运行时环境才会被计算
*/

常量枚举

用 const 声明的枚举就是常量枚举

const enum Month {
  Jan,
  Feb,
  Mar,
}

编译成 JavaScript 代码后

/*
编译后没有任何代码
*/

特点: 在编译阶段会被移除 作用:当我们不需要一个对象,而需要一个对象的值得时候,就可以使用常量的值。这可以减少在编译环境的代码

const enum Month {
  Jan,
  Feb,
  Mar,
}

let month = [Month.Jan, Month.Feb, Month.Mar];

编译成 JavaScript 代码后

var month = [0 /* Jan */, 1 /* Feb */, 2 /* Mar */];

枚举直接被替换成了常量,在运行时的代码就会变得非常简洁

枚举类型

在某些情况下,枚举和枚举成员都可以成为一种单独的类型出现

// 1. 枚举成员没有初始值
enum E {
  a,
  b,
}
// 2. 所有枚举成员都是数字枚举
enum F {
  a = 0,
  b = 1,
}
// 3. 所有枚举成员都是字符串枚举
enum G {
  a = "apple",
  b = "banana",
}

// 将数值赋值给 1,2种情况的枚举,并且值可以超出范围
let e: E = 3;
let f: F = 3;

// 不同类型的枚举是不能进行比较的,会有报错
// e===f

let e1: E = E.a;
let e2: F = F.b;
let e3: E = E.a;

// 不同类型的枚举是不能进行比较的,会有报错
// e1===e2

// 相同类型的枚举是可以进行比较的
e1 === e3;

// 字符串枚举的取值只能是枚举成员的类型
let g1: G = G.a;
let g3: G = G.b;

let g2: G.a = G.a;
// 报错
// let g4: G.a = G.b;