超细致的TypeScript入门与实战

本文是向大家介绍TypeScript基础知识及用法,帮助大家快速了解,在前端项目中使用这门技术。TypeScript的类型推断跟 VS Code 的良好搭配让代码效率有了极大提升。静态类型检测,会提示一些潜在的问题,使得开发者代码更严谨,低级错误早发现早解决。同时TypeScript类型定义使得代码的可读性增强,统一规范,可有效降低项目维护成本。

1.TypeScript简介

什么是TypeScript?

TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。TypeScript通过TypeScript编译器或Babel转译为JavaScript代码,可运行在任何浏览器,任何操作系统。——来自TypeScript官方

1.1 TypeScript类型系统

从TypeScript的名字就可以看出来,「类型」是其最核心的特性。 我们知道,JavaScript是一门非常灵活的编程语言:

它没有类型约束,一个变量可能初始化时是字符串,过一会儿又被赋值为数字。

由于隐式类型转换的存在,有的变量的类型很难在运行前就确定。

基于原型的面向对象编程,使得原型上的属性或方法可以在运行时被修改。

函数可以赋值给变量,也可以当作参数或返回值。

这种灵活性就像一把双刃剑,一方面使得JavaScript蓬勃发展,无所不能。另一方面也使得它的代码质量参差不齐, 维护成本高,运行时错误多。而TypeScript的类型系统,在很大程度上弥补了JavaScript的缺点。

1.2 TypeScript 是静态类型

类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型。

动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。JavaScript 是一门解释型语言,没有编译阶段,所以它是动态类型。

静态类型是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以 TypeScript 是静态类型。

1.3 TypeScript 是弱类型

类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。

TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它们都是弱类型。

在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug。以下这段代码不管是在 JavaScript 中还是在 TypeScript 中都是可以正常运行的:

console.log(1 + '1');  // 打印出字符串 '11'

1.4 适用于任何规模

TypeScript 非常适用于大型项目——这是显而易见的,类型系统可以为大型项目带来更高的可维护性,以及更少的 bug。

在中小型项目中推行 TypeScript 的最大障碍就是认为使用 TypeScript 需要写额外的代码,降低开发效率。但事实上,由于有[类型推论][],大部分类型都不需要手动声明了。相反,TypeScript 增强了编辑器(IDE)的功能,包括代码补全、接口提示、跳转到定义、代码重构等,这在很大程度上提高了开发效率。而且 TypeScript 有近百个[编译选项][],如果你认为类型检查过于严格,那么可以通过修改编译选项来降低类型检查的标准。

TypeScript 还可以和 JavaScript 共存。这意味着如果你有一个使用 JavaScript 开发的旧项目,又想使用 TypeScript 的特性,那么你不需要急着把整个项目都迁移到 TypeScript,你可以使用 TypeScript 编写新文件,然后在后续更迭中逐步迁移旧文件。如果一些 JavaScript 文件的迁移成本太高,TypeScript 也提供了一个方案,可以让你在不修改 JavaScript 文件的前提下,编写一个[类型声明文件][],实现旧项目的渐进式迁移。

1.5 安装TypeScript

TypeScript 的命令行工具安装方法如下:

npm install -g typescript

编译文件:

tsc hello.ts

TypeScript 最大的优势之一便是增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等。主流的编辑器都支持 TypeScript,就连VS Code也是基于TypeScript开发的。所以VS Code对TypeScript的支持也是非常好的,强烈推荐大家使用起来。


2.TypeScript类型

2.1 基本类型

2.1.1 原始数据类型

原始数据类型包括布尔值boolean、数值number、字符串string、undefined、null、void。

还有ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。(最后两种目前运用的比较少,暂不做介绍)

JavaScript中没有空值(Void)的概念,在TypeScript中,可以用void表示没有任何返回值的函数。

undefined、null、void三种的区别是,undefined和null是所有类型的子类型。也就是说undefined和null类型的变量,可以复制给所有类型的变量。而void类型的变量不能赋值给其他类型的变量。

// 1-布尔值

let isDone: boolean = false;

// 2-数值

let dogAge: number = 3;

// 3-字符串

let dogName: string = 'Strawberry';

// 4-空值

function alertName(): void {

  alert(`My dog's name is ${dogName}`);

}

// 5-undefined、null、void三种的区别

let a1: undefined = undefined;

let a2: null = null;

let a3: void;

let b1: number = undefined // ts校验通过

let b2: number = a2 // ts校验通过

let b3: number = a3 // ts报错:不能将类型“void”分配给类型“number”。


2.1.2 any类型和unknown类型

any 和 unknown 在 TypeScript 中是所谓的“顶级类型”。

top type [...]是 通用(universal) 类型,有时也称为 通用超类型,因为在任何给定类型系统中,所有其他类型都是子类型[...]。通常,类型是包含了其相关类型系统中所有可能的[值]的类型。----引用自 Wikipedia:

如果一个值的类型为 any,那么我们就可以用它做任何事。任何类型的值都可以赋值给 any 类型。类型 any 也可被可赋值给每一种类型。

unknown 类型是 any 的类型安全版本。每当你想使用 any 时,应该先试着用 unknown。在 any 允许我们做任何事的地方,unknown 的限制则大得多。在对 unknown 类型的值执行任何操作之前,必须先通过【类型断言】、【相等判断】、【类型防护】、【断言函数】进行限定其类型,否则会ts异常(后续章节会提到)。

使用 any,我们将会失去通常由 TypeScript 的静态类型系统所给予的所有保护。因此,如果我们无法使用更具体的类型或 unknown,则只能将其用作最后的手段。

// demo-1 原始类型都可以赋值给any类型的变量

let age: number = 18;

let any_word: any = 'hello';

// demo-2 在任意值上访问任何属性都是允许的

let anyThing: any = 'Tom';

console.log(anyThing.myName);

console.log(anyThing.myName.firstName);

// demo-3 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

let something;

something = 'seven';

something = 7;

something.setName('Tom');

// demo-4 any和unknown区别

let d1: any = 0

d1.money = 100 // ts校验通过

let d2: unknown = 0

d2.money = 100 // ts报错:unknown类型不存在money属性。


2.1.3 类型推论

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

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

// demo-1 myFavoriteNumber3类型?

let myFavoriteNumber3: any = 'seven'; // 直接定义any类型

myFavoriteNumber3 = 7;

// demo-2 myFavoriteNumber4类型?

let myFavoriteNumber4; // ts推测为any类型

myFavoriteNumber4 = 'seven';

myFavoriteNumber4 = 7;


2.1.4 联合类型

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

let myFavoriteNumber5: string | number;

myFavoriteNumber5 = 'seven';

console.log(myFavoriteNumber5.length); // 5

myFavoriteNumber5 = 7;

console.log(myFavoriteNumber5.length); // ts报错:number类型不存在length属性


2.1.5 对象类型

声明对象类型,经常使用类型别名type和接口interface,二者在使用上的区别:

在类class的类型定义中我们推荐使用接口interface,因为接口interface可以被一个类class实现(implements),但是类型别名type不但不能被extends和implements,就连自己也不能extends和implements其它类型。

在定义简单类型、联合类型、交叉类型、元组时我们用类型别名type来做,并且它和typeof能够天然的结合在一起使用。

// 方式1-接口定义

interface Person {

  name: string;

  age: number;

}

// 方式2-类型别名定义

type Person = {

  name: string;

  age: number;

}

let tom: Person = {

  name: 'Tom',

  age: 25

};


2.1.6 数组类型

常用声明方式:

【类型+方括号】表示方法 string[]

数组泛型 Array<T>

// demo-1 ts只允许数组中包括一种数据类型的值

let arr1: number[] = [1, 2, 3]

let arr2: Array<number> = [1, 2, 3]

// demo-2 如果想为数组添加不同类型的值,需要使用联合类型

let arr3: Array<number | string> = ['aa', 2, 3] // 数组可以同时包括数值和字符串

let arr4: number[] | string[] = [1, 2, 3] // 数组只能全是数值组成,或者全字符串组成

arr4 = ['aa', 'bb']

arr4 = ['aa', 2, 3] // ts报错:赋值类型与定义不一


2.1.7 函数类型

根据JavaScript中定义函数的2种方式:函数声明(Function Declaration)和函数表达式(Function Expression),函数类型写法如下:

// 1. 函数声明(Function Declaration)

function add1(x: number, y: number): number {

  return x + y

}

add1(1, 2);

// 2. 函数表达式(Function Expression)

let add2 = (x: number, y: number): number => {

  return x + y

}

add2(1, 2);

// 可选参数必须接在必需参数后面

function add3(x: number, y?: number): void {

  console.log(x, y)

}

add3(3)


2.1.8 类型断言

主要用于当 TypeScript 推断出来类型并不满足当前需求时,TypeScript 允许开发者覆盖它的推断,可以用来手动指定一个值的类型。类型断言是一个编译时语法,不涉及运行时。

类型断言的常见用途有以下几种:

联合类型可以被断言为其中一个类型

父类可以被断言为子类

任何类型都可以被断言为 any

any 可以被断言为任何类型

要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可

联合类型可以被断言为其中一个类型,类型断言只能够欺骗 TS 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。

interface Cat {

  name: string;

  run(): void;

}

interface Fish {

  name: string;

  swim(): void;

}

function swim(animal: Cat | Fish) {

  (animal as Fish).swim(); // 类型断言,ts校验通过

}

const tom: Cat = {

  name: 'Tom',

  run() { console.log('run') }

};

swim(tom);

// 编译时不会报错,但在运行时会报错 Uncaught TypeError: animal.swim is not a function`

类型断言不是类型转换,它不会真的影响到变量的类型。

// 类型断言

function toBoolean1(something: any): boolean {

  return something as boolean;

}

toBoolean1(1); // 返回值为 1

// 类型转换

function toBoolean2(something: any): boolean {

  return Boolean(something);

}

toBoolean2(1); // 返回值为 true


2.1.9 声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

声明文件必需以 .d.ts 为后缀,声明文件示例:

// src/jQuery.d.ts

declare var jQuery: (selector: string) => any;

推荐使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例: npm install @types/jquery --save-dev

可以通过以下链接搜索需要的声明文件。

https://www.typescriptlang.org/dt/search?search=

如果第三方库没有提供声明文件,我们就需要自己书写声明文件。如何书写声明文件?此处略过,感兴趣的话可以后期尝试。


2.1.10 内置对象

1.ECMAScript 标准提供的内置对象有:

Boolean、Error、Date、RegExp 等。

更多的内置对象,可以查看 MDN 的文档

2.DOM 和 BOM 提供的内置对象有:

Document、HTMLElement、Event、NodeList 等。

而他们的定义文件,则在 TypeScript 核心库的定义文件中。

3.TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。

注意,TypeScript 核心库的定义中不包含 Node.js 部分。

4.用 TypeScript 写 Node.js

Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:

npm install @types/node --save-dev


2.2 高级类型

2.2.1 Type类型别名

类型别名就是给类型起一个新名字。用法类似于接口interface,类型别名常用于原始类型、联合类型,元祖类型等其他必须手动编写的类型,示例:

interface Cat {

  name: string;

  run(): void;

}

interface Fish {

  name: string;

  swim(): void;

}

type Animal = Cat | Fish;


2.2.2 字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个。

// 也可以直接使用字面量进行类型声明

// a只能被赋值为10 不能被赋值为其他值 类似常量 很少使用

let a: 10;

a = 10;

// 字面量形式一般用于或的形式较多

// 可以使用 | 来连接多个类型(类似联合类型)

let b: "male" | "female";

b = "male";

b = "female";


2.2.3 元祖

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

元组起源于函数编程语言(如 F#),这些语言中会频繁使用元组。

// 定义一对值分别为 string 和 number 的元组

let Jerry: [string, number] = ['Jerry', 18];

// 当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型

Jerry.push('male'); // ts校验通过

Jerry.push(true); // ts报错:true和"string|number"类型不匹配


2.2.4 枚举

// demo-1 简单枚举:枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射

enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };

console.log(Days["Sun"] === 0); // true

console.log(Days["Mon"] === 1); // true

console.log(Days["Tue"] === 2); // true

console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true

console.log(Days[1] === "Mon"); // true

console.log(Days[2] === "Tue"); // true

console.log(Days[6] === "Sat"); // true

// demo-2 手动赋值:未手动赋值的枚举项会接着上一个枚举项递增,需要避免覆盖

enum DaysNew { Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat };

console.log(DaysNew["Sun"] === 7); // true

console.log(DaysNew["Mon"] === 1); // true

console.log(DaysNew["Tue"] === 2); // true

console.log(DaysNew["Sat"] === 6); // true


2.2.5 类

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。

public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的

private 修饰的属性或方法是私有的,不能在声明它的类的外部访问

protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

给类加上 TypeScript 的类型很简单,与接口类似:

class Animal {

  public name: string; // 思考1:此处改为private后运行结果?

  public constructor(name: string) {

    this.name = name;

  }

  public sayHi(): string { // 思考2:此处改为private后运行结果?

    return `My name is ${this.name}`;

  }

}

let a: Animal = new Animal('Jack');

console.log(a.sayHi()); // My name is Jack

「构造函数」前加修饰符的区别:

当构造函数修饰为 private 时,该类不允许被继承或者实例化

当构造函数修饰为 protected 时,该类只允许被继承


2.2.6 类与接口

interface Alarm {

  alert(): void;

}

interface Light {

  lightOn(): void;

  lightOff(): void;

}

// demo-1 接口继承接口

interface LightableAlarm extends Light {

  lightOn(): void;

  lightOff(): void;

}

// demo-2 类继承接口

class Car implements Alarm, Light {

  alert() {

    console.log('Car alert');

  }

  lightOn() {

    console.log('Car light on');

  }

  lightOff() {

    console.log('Car light off');

  }

}

// demo-3 接口继承类

interface LittleCar extends Car {

  console(): void;

}

let car1: LittleCar = {

  alert() {

    console.log('Little Car alert');

  },

  lightOn: function (): void {

    console.log('Little Car light on');

  },

  lightOff: function (): void {

    console.log('Little Car light off');

  },

  console: function (): void {

    console.log('Little Car');

  },

}


2.2.7 泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

示例:Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。

function createArray<T>(length: number, value: T): Array<T> {

  let result: T[] = [];

  for (let i = 0; i < length; i++) {

    result[i] = value;

  }

  return result;

}


2.2.8 声明合并

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型。

合并的属性的类型必须是唯一的,如果出现重复,类型不一致会报错。

interface Alarm {

  price: number;

}

interface Alarm {

  weight: number;

}

// 合并后

interface Alarm {

  price: number;

  weight: number;

}

interface Alarm {

  price: string;  // ts报错:类型不一致

  weight: number;

}


3.实战:案例分享

3.1 定义提示

鼠标放上UserInfo都会有相应的定义提示

3.2 定义跳转

在其他页面运用到UserInfo定义时,鼠标放上能快速看到定义类型和注释,点击能直接跳转定义代码


3.3 代码静态检测

根据ts定义自动检测代码,发现错误实时提示,鼠标放上会给出详细信息,并提供快速修复功能。

例如:截图中username没有用驼峰式书写,跟原定义不一致。点击即可自动修复。

3.4 接口定义快速引入项目

推荐安装yapi-to-typescript 插件,快速自动化获取 YApiSwagger 的接口定义,生成 TypeScript 或 JavaScript 的接口类型及其请求函数代码。

插件安装方法:

npm i yapi-to-typescript

生成定义代码命令:

npx ytt

导出模块接口定义步骤:

1.categories中id获取方式:打开yapi项目 --> 点开分类 --> 复制浏览器地址栏 /api/cat_ 后面的数字。

2.yapi上查看项目token方法:

3.ytt.config.ts配置文件参考:

import { defineConfig } from 'yapi-to-typescript';

/* yapi获取ts定义 */

export default defineConfig([

  {

    serverUrl: 'http://XX.XX.XX.XX:XX', // yapi项目地址

    typesOnly: true, // 只生产ts定义

    target: 'typescript',

    reactHooks: {

      enabled: false,

    },

    prodEnvName: 'production',

    outputFilePath: 'api/index.ts', // 定义输出目录

    requestFunctionFilePath: 'api/request.ts',

    dataKey: 'data',

    projects: [

      {

        // yapi项目token,存在有效期限制

        token: 'XXXXXXXXXXXXxx',

        categories: [

          {

            id: 123, // 获取方式:打开项目 --> 点开分类 --> 复制浏览器地址栏 /api/cat_123后面的数字

            getRequestFunctionName(interfaceInfo, changeCase) {

              return changeCase.camelCase(interfaceInfo.parsedPath.name);

            },

          },

        ],

      },

    ],

  },

]);

4.自动生成定义截图:

4.参考文献

官方手册:https://www.typescriptlang.org/docs/handbook/basic-types.html

入门教程:http://ts.xcatliu.com/introduction/what-is-typescript.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容