TypeScript速成小册

840 阅读13分钟

前言

开发者调查分析公司 SlashData 发布了 2022 年《开发者报告》,在连续十次的调查中,JavaScript 受欢迎程度都位居第一

rank.jpeg

JS :感谢曾经瞧不起我的那些语言,打不倒我的令我更强大。

在零几年的时候, js 的程序员常被 JavaC++ 的程序员瞧不上,他们认为 js 就是轻量的脚本语言,但也确实那时候的 js 能做的事寥寥无几。

现今时不同往日咦:

以前说 js 是弱语言,我们现在支持 ts ,强得很(库克:抄袭?);

以前说 js 效率低写不了大工程,我们现在手持三大宝剑:Vue、React、Angular,30s 就能搭好一个工程,成熟的模块化多大都受得了 😎;

以前说 js 只能操作 DOM,我们现在 nodeJS 能写服务,查数据还嗷嗷快;

有感而发,js 真不是以前那个小老弟了,它已长成苍天大树,而前端程序员就是它的枝芽,他们相辅相成。

回观 tstsjs 前行的重要一环,它改变了 js 是弱类型语言的定义,使得对大项目的维护变得简单也提升了开发的效率,所以 ts 是前端程序员必学的知识 。不知道大家是不是跟我一样对 ts 也是 🪴 种草已久,一段时间使用下来给我的感受是,用前膜拜 🙏,初用痛苦 😫,再用舒服 😋,再到最后巴不得所有的项目都上 ts

如果你也刚开始用 ts ,那千万要坚持下去,苦后才会逢甘露。

这里推荐一个在线写 ts 代码的网站:TypeScript 演练场,赶紧上车,开启我们的 ts 之旅 😎。

类型介绍

ts 的类型系统其实不难,大家无需死记硬背,多写多敲,敲代码过程中忘记了就打开文章复习几次自然就都记得了。可以给本文点个赞,写代码时忘了语法可以更快传送回来偷瞄一眼。😳

基础类型

这边直接把所有的类型使用都罗列出来。

/** 原始类型包含:number、string、boolean、null、undefined、symbol */
let num: number = 1;

/** 数组 */

let arrayOfNumber: number[] = [1, 2, 3];
let arrayOfString: string[] = ["x", "y", "z"];

// 泛型写法
let arrayOfNumber2: Array<number> = [1, 2, 3];
let arrayOfString2: Array<string> = ["x", "y", "z"];

/** any */
let anything: any = {};
anything = 1; // 不会提示错误
anything = "x"; // 不会提示错误
// 需要明白且记住:Any is Hell(Any 是地狱)。

/** 元组类型 */
// 表示一个已知元素数量和类型的数组,各元素的类型不必相同
let arr: [string, number] = ["hello", 0];

/** unknown */
let result: unknown;

// unknown 类型仅能赋值给 unknown 或 any 类型
let num1: number = result; // 提示 ts(2322)
let anything1: any = result; // 不会提示错误

// unknown 类型 无法直接调用方法
result.toFixed(); // 提示 ts(2571)
// 需缩小类型范围后才能调用方法
if (typeof result === "number") {
  result.toFixed(); // 此处 hover result 提示类型是 number,不会提示错误
}

/** 类型断言 */
// 在运行代码前,ts仅知道结果可能是 number 或 undefined,所以就报错了
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find((num) => num > 2); // 提示 ts(2322)

// 我们可以这样写
const greaterThan3: number = arrayNumber.find((num) => num > 2) as number;

为函数指定类型

我们一般会为函数指定参数类型和返回值类型

const add = (a: number, b: number): number => {
  return a + b;
};

ts 的 => 用来定义一个函数,而 es6 的 => 是用来实现一个函数,两者结合使用:

type Adder = (a: number, b: number) => number; // TypeScript 函数类型定义
const add: Adder = (a, b) => a + b; // ES6 箭头函数

?: 表示可选参数:

function test(param?: string) {
  console.log(param);
}
test(); // undefined

函数参数支持多类型:

function log3(x: number | string = "hello") {
  console.log(x);
}

剩余参数:

function sum(...nums: number[]) {
  return nums.reduce((a, b) => a + b, 0);
}

sum(1, 2); // => 3
sum(1, 2, 3); // => 6
sum(1, "2"); // ts(2345) Argument of type 'string' is not assignable to parameter of type 'number'

接口类型(interface)

interface 用来定义对象类型和函数类型,通常以大写字母开头。

interface ProgramLanguage {
  /** 语言名称 */
  name: string;
  /** 使用年限 */
  age: () => number;
}

// 约束变量结构
let TypeScript: ProgramLanguage;

// 约束函数入参结构
function NewStudy(language: ProgramLanguage) {
  console.log(
    `ProgramLanguage ${language.name} created ${language.age()} years ago.`
  );
}

?: 可缺省

/** 关键字 接口名称 */
interface OptionalProgramLanguage {
  /** 语言名称 */
  name: string;
  /** 使用年限 */
  age?: () => number;
}
let OptionalTypeScript: OptionalProgramLanguage = {
  name: "TypeScript"
}; // ok

readonly 只读

interface data {
  readonly name: string;
}
let obj: data = {
  name: "张三",
};
/** ts(2540)错误,name 只读 */
data.name = "李四";

函数类型接口

interface StudyLanguage {
  (language: ProgramLanguage): void;
}

/** 单独的函数实践 */
let StudyInterface: StudyLanguage = (language) =>
  console.log(`${language.name} ${language.age()}`);

任意属性

任意属性 就是允许接口有任意的属性。

使用 [xxx: string]: any; 来定义。

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

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

注意

  1. 确定属性和可选属性的类型都必须是任意属性类型的子集
  2. 只能定义一个任意属性

使用任意属性可以定义数组类型,但一般很少这么用

interface NumberArray {
  [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

举个 🌰,类数组的定义:

function sum() {
  let args: number[] = arguments;
}
// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.

可以看到,使用普通方式定义会报错。

我们使用任意属性来处理这个问题:

function sum() {
  let args: {
    [index: number]: number,
    length: number,
    callee: Function
  } = arguments;
}

实际开发中,类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection ,我们直接使用即可。

枚举类型(enum)

es6 中,通常这样定义常量集合

const CAR_MAP = {
  bmw: new Symbol("bmw"),
  byd: new Symbol("byd")
};

ts 中使用枚举来定义 常量集合 ,非常简洁

enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

ts 会将枚举转化为如下 js ,属性为常量、命名值从 0 开始递增数字映射的对象:

var Day = void 0;
(function (Day) {
  Day[(Day["SUNDAY"] = 0)] = "SUNDAY";
  Day[(Day["MONDAY"] = 1)] = "MONDAY";
  Day[(Day["TUESDAY"] = 2)] = "TUESDAY";
  Day[(Day["WEDNESDAY"] = 3)] = "WEDNESDAY";
  Day[(Day["THURSDAY"] = 4)] = "THURSDAY";
  Day[(Day["FRIDAY"] = 5)] = "FRIDAY";
  Day[(Day["SATURDAY"] = 6)] = "SATURDAY";
})(Day || (Day = {}));

开发过程中有 7 种常见的枚举类型

  • 数字类型
  • 字符串类型
  • 异构类型
  • 常量成员和计算(值)成员、枚举成员类型
  • 联合枚举
  • 常量枚举
  • 外部枚举

数字类型

可以给指定成员赋值(不建议对数字类型枚举执行该操作):

enum Day {
    FRIDAY,
    SATURDAY = 5
  }

字符串枚举

enum Day {
    SUNDAY = 'SUNDAY',
    MONDAY = 'MONDAY',
    ...
}

异构枚举

异构枚举 就是支持数字、字符串类型同时使用的枚举。感觉很鸡肋。

enum Day {
    SUNDAY = 'SUNDAY',
    MONDAY = 2,
    ...
  }

常量成员和计算成员

enum FileAccess {
    // 常量成员
    None,
    Read = 1 << 1,
    Write = 1 << 2,
    ReadWrite = Read | Write,
    // 计算成员
    G = "123".length,
  }

我们只需记住缺省值(从 0 递增)、数字字面量、字符串字面量肯定是常量成员

常量枚举

const enum Day {
    SUNDAY,
    MONDAY
  }
  const work = (d: Day) => {
    switch (d) {
      case Day.SUNDAY:
        return 'take a rest';
      case Day.MONDAY:
        return 'work hard';
    }
  }
}

外部枚举

使用 declare 描述一个在其他地方已经定义过的枚举类型,通过这种方式定义出来的枚举类型,被称之为外部枚举:

declare enum Day {
  SUNDAY,
  MONDAY,
}
const work = (x: Day) => {
  if (x === Day.SUNDAY) {
    x; // 类型是 Day
  }
}

泛型

泛型用于在定义函数、接口或类的时候不为其指定具体的类型,在使用的时候再进行指定类型。

使用泛型能让定义的属性更加灵活。

语法

名字<T1, T2, ...>

名字一般表示函数名、接口名、类名T1, T2, ... 表示一个或多个名字任意类型变量,实际开发中常常以首字母大写的标识符作为类型变量名。泛型在使用时必须以真实类型替换类型变量

多类型写法

function reflectExtraParams<P, Q>(p1: P, p2: Q): [P, Q] {
  return [p1, p2];
}

举个 🌰,使用泛型解决输入输出一致问题

定义一个 print 函数用于打印数据:

function print(arg: string): string {
  console.log(arg);
  return arg;
}

这样写会导致打印其他类型时会报错,使用泛型解决:

function print<T>(arg: T): T {
  console.log(arg);
  return arg;
}

多种类型泛型的使用

泛型数组

语法

Array<T>

// 简写
number[]
// 定义数字数组
let arr: number[] = [1, 2, 3];

// 完全等价于
let arr: Array<number> = [1, 2, 3];
function reflectArray<P>(param: P[]) {
  return param;
}
const reflectArr = reflectArray([1, "1"]); // reflectArr 是 (string | number)[]

泛型函数

function identity<T>(m: T): T {
  // T 注解了函数内部的变量定义
  let n: T = m;
  return n;
}

// 调用泛型函数,此时用string类型替换类型变量 T
// identity<string> 作为一个整体相当于一个函数名
let m: string = identity<string>("hello world");

泛型类

// 定义泛型类,包含两个类型变量
class Identity<T1, T2> {
  attr1: T1;
  attr2: T2;
  show(m: T1, n: T2): T2 {
    return n;
  }
}

// 用真实类型替换泛型类的类型变量
// Identity<string, number>作为一个整体相当于一个类名
let a: Identity<string, number>;
// 初始化变量a
a = new Identity<string, number>();
a.attr1 = "hello";
a.attr2 = 99;

// error TS2322: Type '"good"' is not assignable to type 'number'
a.attr2 = "good";

泛型接口

// 定义泛型接口
interface Identity<T> {
  attr: T;
}

// 用真实类型替换泛型接口的类型变量
// Identity<number>作为一个整体相当于一个接口名
let a: Identity<number> = { attr: 10 };
// Identity<string>作为一个整体相当于一个接口名
let b: Identity<string> = { attr: "hello" };

// 错误,类型不匹配,数字10是数字类型,而类型变量为布尔类型
// error TS2322: Type 'number' is not assignable to type 'boolean'.
let c: Identity<boolean> = { attr: 10 };

// 一个复杂点的例子
function fn() {}
let c: Identity<typeof fn> = {
  attr() {}
};

泛型工具

Partial 用于将一个接口的所有属性设置为可选状态,反之,Required 则是将所有属性改为必须状态。

type Person = {
  id: string,
  age: number,
  name: string
};
// 等价{ id?:string, age?:number, name?:string }
type NewPerson = Partial<Person>;

Pick 主要用于提取接口的某几个属性,反之,Omit 用于剔除部分属性。

// 等价 {id: string, age: number }
type NewPickPerson = Pick<Person, "id" | "age">;

泛型约束

使用未知属性报错问题

// 定义泛型函数
function getLength<T>(arg: T): number {
  // 错误,编译器不知道类型变量T是否包含属性length,默认为不存在
  // error TS2339: Property 'length' does not exist on type 'T'
  return arg.length;
}

例子中,并没有明文约束 arg 存在 length 属性,从而 arg.length 导致了异常。

泛型约束就是用来解决该问题。

泛型约束语法

<T extends xx类型>

类型约束关键字为 extends,和继承关键字一样。实际上,类型约束跟继承同义,类型变量继承了被约束类型的所有成员

使用泛型约束解决未知属性问题

// 声明接口
interface WithLength {
  length: number;
}

// 正确,T现在被接口类型WithLength约束,包含属性 length
function getLength<T extends WithLength>(arg: T): number {
  return arg.length;
}

默认类型

语法

<T = string>

举例

interface MyType<T = string> {
  value: T;
}

// 正确,在类型参数没有显示指定的情况下,采用了默认类型 string
let x1: MyType = {
  value: "hello world"
};
// 等价于
let x1: MyType<string> = {
  value: "hello world"
};

// 错误, error TS2322: Type 'number' is not assignable to type 'string'
let x2: MyType = {
  value: 123
};

// 正确,覆盖默认的 string 类型
let x3: MyType<number> = {
  value: 123
};

类型进阶

类型推断、字面量类型

啥是类型推断?ts 会自动判断变量或返回值的类型。

let num: number = 1;
// 等价于
let num = 1;

初始化变量值、函数参数默认值、函数返回值等都会自动类型推断。

/** 推断参数 num 的类型是数字或者 undefined,返回值的类型也是数字 */
function getNum(num = 1) {
  return num;
}

字面量类型 ts 支持 字符串、数字、布尔值 三种字面量类型,来看个例子:

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

字面量类型时集合类型的子集。

字面量类型能赋值给集合类型,但是反之是不可行的:

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

通常会结合联合类型使用:

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'

数字字面量和布尔值字面量也是类似用法:

interface config {
  size: "small" | "big";
  margin: 0 | 10;
  isEnable: false | true;
}

let、const 定义变量值相同但类型不一致问题

let str = "hello"; // str: string
const str2 = "hello"; // str2: 'hello'

这是由于 const 定义变量值不会改变,这样就缩小了变量的类型范围。

类型断言

语法

// 1、尖括号语法
<类型表达式>值;

// 2、as语法as 类型表达式;

为了避免和 JSX 语法产生冲突,尖括号语法只能在 tsx 文件中使用

let someValue: any = "this is a string";

// 1、尖括号语法
let strLength: number = (<string>someValue).length;
// 2、as语法
let strLength: number = (someValue as string).length;

type 类型别名

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

别名不会创建一个新的类型,它只是原类型的一个引用,和原类型完全等价,它的定义方式有点类似 let 。

语法: type 别名 = 类型

合法的类型别名声明:

// 数字类型别名
type myNumber = number;
// 布尔类型别名
type myBoolean = boolean;
// 联合类型别名
type transition = "EASE" | "EASEIN" | "EASEOUT";
// 联合类型别名
type StringOrNumber = string | number;
// 联合类型别名
type Text = string | { text: string };
// 泛型的实际类型别名
type NameLookup = Dictionary<string, Person>;
// 通过类型查询定义别名
type ObjectStatics = typeof Object;
// 泛型函数别名
type Callback<T> = (data: T) => void;
// 元组泛型别名
type Pair<T> = [T, T];
// 泛型的实际类型别名
type Coordinates = Pair<number>;
// 联合类型别名
type Tree<T> = T | { left: Tree<T>; right: Tree<T> };

声明了别名以后,别名就相当于是一个类型的标识符,可以用于注解语法中:

// 声明transition为联合类型的别名
type transition = "EASE" | "EASEIN" | "EASEOUT";

// transition此时是一个类型标识符
const boxTransition: transition = "EASE";

keyof 类型索引

interface A {
  a: string;
  b: number;
}
// 等效于 'a' | 'b'
type customType = keyof A;
let param: customType = "a";

| & 高级类型:联合、交叉、合并接口类型

联合类型| 表示或。

交叉类型& 表示且。

type test = string | number;
// 没啥意义,一般在合并接口时才用 &
type test = string & number;

// 具体值的联合类型
type girlName = "张胜男" | "王建国";
type boyName = "王建国" | "李世平";

type nameGroup = girlName | boyName;

let newName: nameGroup = "张胜男"; // ok , '王建国'、'李世平'也可以

type nameGroup2 = girlName & boyName;
let newName2: nameGroup2 = "王建国"; // ok , 其他值都报错

联合、交叉组合

联合操作符 | 的优先级低于交叉操作符 &

type UnionIntersectionA =
  | ({ id: number } & { name: string })
  | ({ id: string } & { name: number }); // 交叉操作符优先级高于联合操作符

type UnionIntersectionB =
  | ("px" | "em" | "rem" | "%")
  | ("vh" | "em" | "rem" | "pt"); // 调整优先级

合并接口类型

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

其他

declare

在 ts 中使用 js 的 npm 库,ts 校验会不通过。可以使用 declare 关键字声明全局的变量、方法、类、对象。

语法:

declare (var|let|const) 变量名称: 变量类型

// 变量
declare let amount: number;
// 函数
declare function toString(x: number): string;
// 类
declare class Person {
  public name: string;
  private age: number;
  constructor(name: string);
  getAge(): number;
}
// 枚举
declare enum Direction {
  Up,
  Down,
  Left,
  Right,
}

amount = 1

注意:使用 declare 关键字时,我们不需要编写声明的变量、函数、类的具体实现(因为变量、函数、类在其他库中已经实现了),只需要声明其类型即可

// TS1183: An implementation cannot be declared in ambient contexts.

declare function toString(x: number) {
  return String(x);
};

declare namespace

命名空间用于描述复杂的全局对象。

declare namespace $ {
  const version: number;

  function ajax(settings?: any): void;
}

$.version; // => number
$.ajax();

该例子声明了全局导入的 JQuery 变量 $

@types

我们在使用第三方库的时候,如 jQuery ,引入后直接使用

$.get(URL, callback);

这会让 ts 一头雾水而导致报错,因为 ts 不知道何为 $ ,更不知道 $ 有哪些属性方法,声明文件 的作用就是让 ts 识别到第三方的特性。

一般做法是定义好三方暴露的对象的特性,然后集成到 ts 类型系统,开发过程中 ts 能非常友好的提醒你第三方有哪些属性可供使用。目前社区已拥有非常丰富的三方类型定义的库。

DefinitelyTyped 是一个高质量的 TypeScript 类型定义的仓库。

一般通过 npm 来安装使用 @types ,例如为 jquery 添加声明文件:

npm install @types/jquery --save-dev

全局 @types

默认情况下,TypeScript 会自动包含支持全局使用的任何声明定义。例如,对于 jquery,你应该能够在项目中开始全局使用 $。

模块 @types

import * as $ from "jquery";

// 现在你可以此模块中使用$

后语

ts 的介绍就到这,虽然篇幅介绍的内容没有很深入,但以入门来说广度还是足够的。本文旨在抛砖引玉让更多人入门 ts ,让我们 ts 社区更加活跃,ts 越来越规范化。

对文章内容有疑问可在评论区留言,本文如果对你有一点点帮助你的点赞 👍🏻 就是对我最大的支持, peace 🤙

更多 ts 学习资料

typescript-tutorial (Star 8.9k)

深入理解 TypeScript (Star 5.5k)

ts 规范,看到 clean-code ,dddd(懂的都懂):clean-code-typescript (Star 6.1k)

TypeScript 官网翻译 (出自冴羽)

TypeScript 速成教程 (Star 0.4k)