likes
comments
collection
share

TypeScript:入门与进阶(一)

作者站长头像
站长
· 阅读数 10

为什么学习typescript

  • 让程序更容易理解
    • 能直观的了解函数或者方法输入输出的类型等
    • 减少手动调试过程,在编译阶段就减少了错误的产生
  • 使用IDE提示功能,效率更高
    • 在不同的代码块喝定义中进行跳转
    • 代码自动补全

一、简介

1、什么是TypeScript

  • 是一个javascript超集,是一门静态类型、弱类型的语言
  • 不能直接在浏览器或者node环境运行,需要编译成js代码后才可以运行
  • TypeScript 是一门静态类型、弱类型的语言。

2、TypeScript优势

  • 在开发过程中,有更好的错误提示,发现潜在问题
  • 更好的编辑器自动提示
  • 类型声明可以直接看到代码的语义

3、vscode准备

首选项-设置:

  • 搜索 quote ,设置为单引号 TypeScript:入门与进阶(一)
  • 搜索 tab ,设置为两个字符长度

TypeScript:入门与进阶(一)

  • 安装插件 prettier

TypeScript:入门与进阶(一)

搜索 save ,勾选 Formate On Save

TypeScript:入门与进阶(一)

4、nrm准备

npm install nrm -g

MAC安装时课可能有权限问题,则输入命令:

sudo npm install nrm -g

安装完成之后,执行命令nrm ls,可以看到有如下图所示的内容,指的是包依赖的地址。 TypeScript:入门与进阶(一) 默认的包依赖是npm,因为npm是国外的地址,下载速度会比较慢,所以可以更换包依赖 执行命令nrm use cnpm,之后下载的包依赖,就是从国内的镜像地址安装,速度会快很多。 TypeScript:入门与进阶(一)

5、安装TypeScript

5.1、安装

npm install -g typescript

使用MAC,安装时会报错,如下图:

TypeScript:入门与进阶(一)

同时,也给了解决办法。会报错是因为执行命令没有获取管理员权限,只需要在原来的命令前面加上sudo获取权限,然后回车输入管理员密码即可。

sudo npm install -g typescript

TypeScript:入门与进阶(一)

输入tsc命令查看,安装成功。

TypeScript:入门与进阶(一)

5.2、测试ts基础环境搭建

新建 demo.ts 文件

interface Point { x: number, y: number }

function tsDemo(data: Point) {
  console.log(123);
  return data.x + data.y;
}

tsDemo({ x: 1, y: 2 })

执行tsc demo.ts 命令,会生成一个demo.js文件,再执行 node demo.js 命令,控制台会有输出,如下图所示:

TypeScript:入门与进阶(一)

到这里,说明 typeScript 环境搭建成功。

5.3、简化运行ts文件

安装 ts-node

sudo npm install -g ts-node

执行命令 ts-node demo.ts即可运行这个ts文件。相当于合并执行了tsc demo.tsnode demo.js两个命令。

6、TS代码执行原理

TS默认在node和浏览器中无法执行,因为它们底层都是V8引擎。需要通过tsc命令转换成js文件,就可以运行。

TS特点:

  • TS主要在JS的基础上增加了Static Type Checker(静态类型校验)能力
  • 静态校验能力:一门语言在代码执行之前,就能做出错误预警。
  • TS 约等于JS + Static Type Checker

TS底层实现逻辑(解析流程):

在编译器里编写正常的JS代码时,是没有静态校验能力的,但是编译器(vscode)增加了静态类型校验能力,所以在编译阶段就能给出错误预警。

js+类型 =》`Static Type Checker(静态类型校验)能力`=》代码执行前报错

二、TS+ webpack 项目搭建

1、生成 package.json 文件

npm init -y

2、生成 tsconfig.json 文件

tsc --init

3、安装 ts-node

如果全局有 ts-node包,可以先卸载 npm uninstall ts-node -g,再在本项目下重新安装依赖

npm install -D ts-node

4、安装 typescript

如果全局有 typescript包,可以先卸载 npm uninstall typescript -g,再在本项目下重新安装依赖

npm install -D typescript

5、在 package.json 文件中配置开发编译打包命令

 "scripts": {
    "dev": "ts-node ./src/main.ts"
  },

6、测试项目配置是否完成

新建 src/main.ts 文件,编写代码

console.log('hello word');

7、运行 npm run dev,如果有如图所示的打印输出,则说明环境配置完成。

TypeScript:入门与进阶(一)

8、编译打包

编辑tsconfig.json 文件的 outDir,指定输出目录

TypeScript:入门与进阶(一)

package.json 文件中添加打包命令

"scripts": {
    "dev": "ts-node ./src/main.ts",
    "build": "tsc"
  },

执行 npm run build 即可以将 ts 文件编译成 js 文件。

TypeScript:入门与进阶(一)

9、热更新

开发过程中,可能希望有工具可以帮助开发者实时监控ts文件改变,自动运行 tsc 命令,编译 ts 代码,提高开发效率。

更改package.json 中的打包命令

"scripts": {
    "build": "tsc -w"
  },

也可以用 nodemon + concurrently 工具检测文件更改,重新启动应用程序。

三、TypeScript基础

1、类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型。而TypeScript 是静态类型。 2、类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它们都是弱类型

1、静态类型的深度理解

  • 如果定义一个变量为number类型,那这个变量具有numer类型的所有方法,其他类型也如此。 TypeScript:入门与进阶(一)

  • 如果定义一个变量为自定义类型,那这个变量具有自定义类型的所有方法和属性 TypeScript:入门与进阶(一)

  • 如果一个变量是一个静态类型,不仅类型无法更改,而且此变量的属性与方法已经确定

2、类型分类

最基础的部分可参考相关文档。 这里主要记录一些容易忽略的部分。

2.1、基础类型

作用:给变量定义基础类型,可以帮助开发者更直观的判断变量或属性的内容。 基础类型有:number, boolean, string, null, undefined, symbol, void

2.2、对象类型

1. 对象

const teacher: {
  name: string;
  age: number;
} = {
  name: 'Dell',
  age: 18
};

2. 数组 最简单的方法是使用「类型 + 方括号」来表示数组。

const numbers: number[] = [1, 2, 3];

3. 类

class Person {}

const dell: Person = new Person();

4. 函数

const getTotal: () => number = () => {
  return 123;
};

5. Date

const date = new Date();
};

2.3、字面量类型(Literal Types)

字面量类型(Literal Types),它代表着比原始类型更精确的类型,同时也是原始类型的子类型。

字面量类型主要包括字符串字面量类型数字字面量类型布尔字面量类型对象字面量类型,它们可以直接作为类型标注:

const str: "linbudu" = "linbudu";
const num: 599 = 599;
const bool: true = true;

单独使用字面量类型比较少见,因为单个字面量类型并没有什么实际意义。它通常和联合类型(即这里的 |)一起使用,表达一组字面量类型:

interface Tmp {
  bool: true | false;
  num: 1 | 2 | 3;
  str: "lin" | "bu" | "du"
}

3、类型注解和类型推断

3.1、类型注解(type annotation)

开发者直接告诉TS 变量是什么类型

let count: number;
count = 123;

3.2、类型推断(type inference)

TS 会自动的去尝试分析变量的类型。

let countInference = 123;

编译器会自动提示 countInferencenumber 类型。

TypeScript:入门与进阶(一)

3.3、总结

  • 如果 TS 能够自动分析变量类型,开发者就什么也不需要做了。
  • 如果 TS 无法分析变量类型的话,开发者就需要使用类型注解。 TypeScript:入门与进阶(一)

4、函数相关类型

4.1、函数返回值为基本类型 函数的返回值可以是number、boolean、string等

// 参数与返回值进行类型注解 
function add(first: number, second: number): number {
  return first + second;
}

4.2、函数无返回值

function sayHello():void {
  console.log('hello');
}

4.3、函数返回值为never 函数永远不可能执行到最后。

function errorEmitter():never {
    throw new Error();
  // while(true) {}
}

4.4、函数参数解构进行类型注解

// 函数参数解构  类型注解
function  add({ first, second }: { first: number, second: number }): number {
  return first + second;
}

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

const total = add({ first: 1, second: 2 });
const count = getNumber({ first: 1 });

4.5、函数表达式

冒号【:】后(str: string) => number 是说明函数参数与返回值类型,而【=】后是函数的类型。

const func1: (str: string) => number = str => {
  return parseInt(str, 10);
};

5、数组与元组

5.1、数组

  • 1、数组中存储基础数据类型
const arr: (number | string)[] = [1,'2',3];

const stringArr: string[] = ['1','2']

const undefinedArr: undefined[] = [undefined];
  • 2、存储对象类型
const objectArr: { name: string }[] = [
  { name: 'dell' }
]

存储对象数组类型时,可以使用 类型别名(type alias) 来表示对象数组类型。

// 类型别名
type User = { name: string, age: number };

const objectArr: User[] = [
  { name: '123', age: 6 }
]

同时,也可以用 类(class) 来表示。

class Teacher {
  name: string;
  age: number;
}

const objectArr: Teacher[] = [
  new Teacher(),
  {
    name: 'dell',
    age: 28
  }
];

5.2、元组

一个数组的长度是固定的,数组中的每一项的类型也是固定的,就可以使用元组管理。

const teacherInfo: [string, string, number] = ['Dell', 'male', 18];

对于一些固定类型的二维数组在声明类型时,元组刚好满足需求: TypeScript:入门与进阶(一)

6、接口(interface)

常用于对「对象的形状(Shape)」进行描述。 接口的属性有哪些呢? 6.1、普通属性

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

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

6.2、只读属性

interface Person {
    readonly id: number;
    name: 'tom'
}

let tom: Person = {
    id: 123,
};

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候详见官方文档

6.3、可选属性

interface teacher {
  readonly id: number;
  name: string;
  // 可选属性
  age?: number;
}

6.4、任意属性

interface teacher {
  readonly id: number;
  name: string;
  // 可选属性
  age?: number;
  // 任意属性
  [propName: string]: string | number;
}

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

interface teacher {
  readonly id: number;
  name: string;
  // 可选属性
  age?: number;
  // 任意属性
  // 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
  [propName: string]: string | number;
}

const person: teacher = {
  id: 1,
  name: 'ming',
  // 任意属性
  grade: 2
}

6.5、接口也可以定义一个函数类型

// 定义一个函数类型的接口
interface SayHi {
  // 函数的参数是一个string类型,返回一个string类型
  (word: string): string;
}

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

7、类(class)

7.1、类的定义

class Person {
  name = 'dell';
  getName() {
    return this.name;
  }
}

const teacher = new Teacher();
console.log(teacher.getName());// 'dell'

7.2、类的继承

对于这里的两个类,比较严谨的称呼是 基类(Base  与 派生类(Derived 。关于基类与派生类,我们需要了解的主要是派生类对基类成员的访问与覆盖操作

class Person {
  name = 'dell';
  getName() {
    return this.name;
  }
}

// 继承
class Teacher extends Person {
  getTeacherName() {
    return 'Teacher';
  }
}

const teacher = new Teacher();
console.log(teacher.getName()); // 'dell'
console.log(teacher.getTeacherName());// ‘Teacher’

7.3、类的方法的重写

class Person {
  name = 'dell';
  getName() {
    return this.name;
  }
}

// 继承
class Teacher extends Person {
  getTeacherName() {
    return 'Teacher';
  }
  // 子类方法重写
  getName() {
    return 'lee';
  }
}

const teacher = new Teacher();
console.log(teacher.getName()); // 'lee'

派生类覆盖 基类方法时,我们并不能确保派生类的这一方法能覆盖基类方法,万一基类中不存在这个方法呢?所以,TypeScript 4.3 新增了 override 关键字,来确保派生类尝试覆盖的方法一定在基类中存在定义:

// 继承
class Teacher extends Person {
  getTeacherName() {
    return 'Teacher';
  }
  // 子类方法重写
  override getName() {
    return 'lee';
  }
  // 此处会有错误提示
  override getAge() {
    return 20;
  }
}

在这里 TS 将会给出错误,因为尝试覆盖的方法并未在基类中声明。通过这一关键字我们就能确保首先这个方法在基类中存在,同时标识这个方法在派生类中被覆盖了。

TypeScript:入门与进阶(一)

7.4、类的方法被重写后,super重新调用父类方法

class Person {
  name = 'dell';
  getName() {
    return this.name;
  }
}

// 继承
class Teacher extends Person {
  getTeacherName() {
    return 'Teacher';
  }
  // 子类方法重写
  getName() {
    // super是指Person父类  super.getName()调用的是父类的getName()方法
    return super.getName() + ' lee';
  }
}

const teacher = new Teacher();
console.log(teacher.getName()); // 'dell lee'

7.5、类的修饰符和构造器

TypeScript 中我们能够为Class 成员添加这些修饰符:public / private / protected / readonly。除 readonly 以外,其他三位都属于访问性修饰符,而 readonly 属于操作性修饰符(就和 interface 中的 readonly 意义一致)。

修饰符:private、protected、public

  • public:此类成员在类、类的实例、子类中都能被访问。
  • private:此类成员仅能在类的内部被访问。
  • protected:此类成员仅能在类与子类中被访问

类的构造器 类在实例化(new 操作)时,类的构造器(constructor)会执行。

初始化类的属性的传统写法:

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

const person = new Person('dell');
console.log(person.name);// dell

初始化类的属性,还有另一个简写写法:

cclass Person {
  // 传统写法
  // public name: string;
  // constructor(name: string) {
  //   this.name = name;
  // }
  // 简化写法
  constructor(public name: string) {}
}

也就是说,只要在构造器的接收参数加上 public 关键字,就可以初始化类的属性。子类也可以继承。 但是如果子类有构造器,必须调用super(),否则会报错

class Person {
  constructor(public name: string) {}
}

class Teacher extends Person {
  constructor(public age: number) {
    super('dell');
  }
}

const teacher = new Teacher(28);
console.log(teacher.age);
console.log(teacher.name);

7.6、类的静态成员

TypeScript 中,你可以使用static关键字来标识一个成员为静态成员:

class Foo {
  static staticHandler() { }

  public instanceHandler() { }
}

不同于实例成员,在类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler 这种形式进行访问。

类的静态属性

1、getter:访问私有属性

class Person {
  constructor(private _name: string) {}
  // 可访问内部属性,确保属性的私有性
  get name() {
    return this._name;
  }
}

const person = new Person('dell');
console.log(person.name);//'dell'

2、setter:重置私有属性

class Person {
  constructor(private _name: string) {}
  // 可访问内部属性,确保属性的私有性
  get name() {
    return this._name;
  }
  // 设置私有属性
  set name(name: string) {
    const realName = name.split(' ')[0]
    this._name = realName;
  }
}

const person = new Person('dell');
console.log(person.name);

person.name = 'dell lee'

防止类的外部修改类内部的属性,除了设置为private属性,也可以设置为readonly

class Person {
  public readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
}
const person = new Person('Dell');
console.log(person.name);// 'Dell'
// 会报错
// person.name = 'hello'

只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。 3、单例模式

class Demo {
  // instance 存储new 出来的demo
  private static instance: Demo;
  // 单例模式  不允许外部通过new 的形式实例化
  private constructor(public name: string) {}
  // 类的属性  不是类的实例属性
  static getInstance() {
    // 没有instance,就创建一个实例
    if (!this.instance) {
      this.instance = new Demo('dell lee');
    }
    // 有实例,就返回
    return this.instance;
  }
}

// 返回一个唯一的实例
const demo1 = Demo.getInstance();
const demo2 = Demo.getInstance();
console.log(demo1.name);// 'dell lee'
console.log(demo2.name);// 'dell lee'

console.log(demo1 == demo2);// true

7.7、抽象类

定义: abstract 用于定义抽象类和其中的抽象方法。 详细文档介绍可自行查看

抽象类是对类结构与方法的抽象,简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等)一个抽象方法描述了这一方法在实际实现中的结构

类的方法和函数非常相似,包括结构,因此抽象方法其实描述的就是这个方法的入参类型返回值类型

abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}

注意,抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员,如这里的抽象方法。我们可以实现(implements)一个抽象类:

class Foo implements AbsFoo {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

此时,我们必须完全实现这个抽象类的每一个抽象成员。需要注意的是,在 TypeScript 中无法声明静态的抽象成员

抽象类是不允许被实例化

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}
// 实例化时,会报错
//let a = new Animal('Jack');

抽象类中的抽象方法必须被子类实现

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}

let cat = new Cat('Tom');

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

7.8、声明类的结构-interface

1. implements

interface FooStruct {
  absProp: string;
  get absGetter(): string;
  absMethod(input: string): string
}

class Foo implements FooStruct {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

const foo = new Foo()

console.log(foo.absProp) // linbudu

2. Newable Interface

class Foo {
  constructor(public name: string) {}
 }

interface FooStruct {
  new(): Foo
}

declare const NewableFoo: FooStruct;

const foo = new NewableFoo();

8、类与接口

8.1、类实现(implements)接口

实现(implements)是面向对象中的一个重要概念.

interface Person {
  // readonly id: number;
  name: string;
  // 可选属性
  age?: number;
  // 任意属性
  // 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
  [propName: string]: any;
  say(): string;
}

// 类实现(implements)接口
class User implements Person {
  name = 'dell';
  say() {
    return 'hello'
  }
}

一个类还可以实现多个接口

interface Person {
  // readonly id: number;
  name: string;
  // 可选属性
  age?: number;
  // 任意属性
  // 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
  [propName: string]: any;
  say(): string;
}

interface Teacher {
  grade: number
}

class User1 implements Person, Teacher {
  name = 'dell';
  grade = 3
  say() {
    return 'hello'
  }
}

8.2、接口继承接口

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

8.3、接口继承类

为什么 TypeScript 会支持接口继承类呢?

实际上,当我们在声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。

所以我们既可以将 Point 当做一个类来用(使用 new Point 创建它的实例):

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

9、内置类型:any 、unknown 与 never

9.1、any类型

any 类型的主要意义,其实就是为了表示一个无拘无束的“任意类型”,它能兼容所有类型,也能够被所有类型兼容

避免 any 滥用的方法:

  • 如果是类型不兼容报错导致你使用 any,考虑用类型断言替代
  • 如果是类型太复杂导致你不想全部声明而使用 any,考虑将这一处的类型去断言为你需要的最简类型。
  • 如果你是想表达一个未知类型,更合理的方式是使用 unknown

9.2、unknown 类型

unknown 类型和 any 类型有些类似,一个 unknown 类型的变量可以再次赋值为任意其它类型,但只能赋值给 anyunknown 类型的变量:

let unknownVar: unknown = "linbudu";

unknownVar = false;
unknownVar = "linbudu";
unknownVar = {
  site: "juejin"
};

unknownVar = () => { }

const val1: string = unknownVar; // Error
const val2: number = unknownVar; // Error
const val3: () => {} = unknownVar; // Error
const val4: {} = unknownVar; // Error

const val5: any = unknownVar;
const val6: unknown = unknownVar;

TypeScript:入门与进阶(一)

9.3、any 与 unknow区别

any 放弃了所有的类型检查,而 unknown 并没有。这一点也体现在对 unknown 类型的变量进行属性访问时:

let unknownVar: unknown; 
unknownVar.foo(); // 报错:对象类型为 unknown

要对 unknown 类型进行属性访问,需要进行类型断言,即“虽然这是一个未知的类型,但我跟你保证它在这里就是这个类型!”:

let unknownVar: unknown;

(unknownVar as { foo: () => {} }).foo();

在类型未知的情况下,更推荐使用 unknown 标注。

9.4、虚无的 never 类型

never 才是一个“什么都没有”的类型,它甚至不包括空的类型,严格来说,never 类型不携带任何的类型信息,在联合类型中被直接移除。

type UnionWithNever = "linbudu" | 599 | true | void | never;

将鼠标悬浮在类型别名之上,你会发现这里显示的类型是"linbudu" | 599 | true | void。never 类型被直接无视掉了,而 void 仍然存在。这是因为,void 作为类型表示一个空类型,就像没有返回值的函数使用 void 来作为返回值类型标注一样,void 类型就像 JavaScript 中的 null 一样代表“这里有类型,但是个空类型”。 TypeScript:入门与进阶(一)

10、类型断言:警告编译器不准报错

类型断言能够显式告知类型检查程序当前这个变量的类型,可以进行类型分析地修正、类型。它其实就是一个将变量的已有类型更改为新指定类型的操作,它的基本语法是 as NewType

let unknownVar: unknown;

(unknownVar as { foo: () => {} }).foo();

还可以 asany 来为所欲为,跳过所有的类型检查:

const str: string = "linbudu"; 
(str as any).func().foo().prop;

也可以在联合类型中断言一个具体的分支:

function foo(union: string | number) {
  if ((union as string).includes("linbudu")) { }

  if ((union as number).toFixed() === '599') { }
}

类型断言的正确使用方式是,在 TypeScript 类型分析不正确或不符合预期时,将其断言为此处的正确类型:

interface IBar {
  name: string;
}

declare const obj: {
  bar: IBar
}

// 方式一:
const {
  bar = {} as IBar
} = obj

// 方式二:
// const {
//   bar = <IBar>{}
// } = obj

更严谨的方式应该是定义为 Partial<IFoo> 类型,即 IFoo 的属性均为可选的:

interface IBar {
  name: string;
}

declare const obj: {
  // foo: IBar
  bar: Partial<IBar>
}

const { bar } = obj

10.1、双重断言

如果在使用类型断言时,原类型与断言类型之间差异过大,也就是指鹿为马太过离谱,离谱到了指鹿为霸王龙的程度,TypeScript 会给你一个类型报错:

const str: string = "linbudu";

// 从 X 类型 到 Y 类型的断言可能是错误的
(str as { handler: () => {} }).handler()

错误提示如下: TypeScript:入门与进阶(一) 此时它会提醒你先断言到 unknown 类型,再断言到预期类型

const str: string = "linbudu";

// 方式一:
(str as unknown as { handler: () => {} }).handler()
// 方式二:
// (<{ handler: () => {} }>(<unknown>str)).handler()

10.2、非空断言

非空断言其实是类型断言的简化,它使用 ! 语法,即 obj!.func()!.prop 的形式标记前面的一个声明一定是非空的(实际上就是剔除了 nullundefined 类型):

declare const foo: {
  func?: () => ({
    prop?: number | null;
  })
};

// foo.func().prop.toFixed();// Error
// 非空断言
foo.func!().prop!.toFixed();

非空断言的常见场景还有 document.querySelectorArray.find 方法等:

const element = document.querySelector("#id")!; 
const target = [1, 2, 3, 599].find(item => item === 599)!;

使用了非空断言时,就确定了变量的类型,如图所示: TypeScript:入门与进阶(一) 如果未使用非空断言,则没有剔除 nullundefined 类型: TypeScript:入门与进阶(一)

四、TypeScript中的配置文件

初始化一个项目,执行 tsc --init 会生成 tsconfig.json 文件,那这个文件的作用是什么呢?

其实,这是 typescript的编译文件。执行命令 tsc 文件名,就可以生成对应的ts文件。如果直接执行 tsc 命令,则会把根目录下所有的ts文件都进行编译。

为什么 执行 tsc 命令,会编译所有的ts文件呢? tsconfig.json 会默认将 根目录下的ts文件 统一进行编译。

当执行tsc 文件名这种命令时,不会按照tsconfig.json文件的配置项进行编译,只有执行tsc 命令,不带任何参数时,才可以。

如果只想要某个文件按照自己的配置项进行编译,则可以在tsconfig.json配置 include 这个配置项,指定tsconfig.json配置文件,只对 include 下的 文件生效。执行 tsc 命令时,也只编译了对应文件,其他文件不编译。

{
  // 配置要编译的文件 只编译demo.ts,其他文件不编译
  "include": ["demo.ts"],
  "compilerOptions": {}
}

也可以使用 exclude 排除某些文件不编译。

如果想要对目标文件进行特殊编译配置,可以对 tsconfig.json文件的 compilerOptions属性进行配置,比如:

  • "removeComments": true:编译时,去掉注释。
  • "noImplicitAny": true:明确指定any类型,
  • "strictNullChecks": true:开启检验null、undefined类型

更多的配置文件说明参考 tsconfig官方文档以及配置项

使用 ts-node工具运行 ts文件 ,也会使用 tsconfig.json配置内容对文件进行编译。

1、常用配置项字段解析

  • "baseUrl": "./" :项目的根路径
  • rootDir: "./src":启动 tsc 命令,则对src下的文件进行编译
  • "outDir": "./dist/",":启动 tsc 命令,编译输出文件都在 dist目录下
  • "incremental": true:增量编译,只编译新增的内容,之前编译过的,不再编译
  • "allowJs": true, : 将js文件按照es5的语法编译
  • "removeComments": true:编译时,去掉注释。
  • "noImplicitAny": true:明确指定any类型,
  • "strictNullChecks": true:开启检验null、undefined类型
  • "noUnusedParameters": true:开启函数参数的校验

五、TypeScript进阶

1、类型工具

如果按照使用方式来划分,ts类型工具可以分成三类:操作符、关键字与专用语法。 而按照使用目的来划分,ts类型工具可以分为 类型创建 与 类型安全保护 两类。

TypeScript:入门与进阶(一)

1.1、类型别名

通过 type 关键字声明了一个类型别名。类型别名的作用主要是对一组类型或一个特定类型结构进行封装,以便于在其它地方进行复用。

type A = string;

// 抽离一组联合类型
type StatusCode = 200 | 301 | 400 | 500 | 502;
type PossibleDataTypes = string | number | (() => unknown);

const status: StatusCode = 502;

// 抽离一个函数
type Handler = (e: Event) => void;

const clickHandler: Handler = (e) => { }; 
const moveHandler: Handler = (e) => { }; 
const dragHandler: Handler = (e) => { };

// 声明一个对象类型
type ObjType = {
  name: string;
  age: number;
}

1.2、工具类型

类型别名中,类型别名可以这么声明自己能够接受泛型。一旦接受了泛型,我们就叫它工具类型

type Factory<T> = T | number | string;

// 此时FactoryWithBool的类型是: boolean|number|string
type FactoryWithBool = Factory<boolean>;

// 声明一个包含null 的联合类型
type MaybeNull<T> = T | null;


type MaybeArray<T> = T | T[];

// 函数泛型
function ensureArray<T>(input: MaybeArray<T>): T[] {
  return Array.isArray(input) ? input : [input];
}

对于工具类型来说,它的主要意义是基于传入的泛型进行各种类型操作,得到一个新的类型。

1.3、联合类型

联合类型可以理解为,它代表了一组类型的可用集合,只要最终赋值的类型属于联合类型的成员之一,就可以认为符合这个联合类型。

联合类型:A | B,只需要实现 AB 即可:

type foo = string | number

联合类型(Union Types)表示取值可以为多种类型中的一种。

interface Bird {
  fly: boolean;
  sing: () => {};
}

interface Dog {
  fly: boolean;
  bark: () => {};
}

function trainAnimal (animal: Bird|Dog) {}

当使用联合类型时,只能访问 联合类型的所有类型里共有的属性或方法

TypeScript:入门与进阶(一)

1.4、交叉类型

交叉类型: A & B,需要同时满足 AB 两个类型才行:

interface NameStruct {
  name: string;
}

interface AgeStruct {
  age: number;
}

type ProfileStruct = NameStruct & AgeStruct;

// 同时包含name age两个属性 缺少任意一个会报错
const profile: ProfileStruct = {
  name: "linbudu",
  age: 18
}

如果是两个原始类型交叉,则得到的是一个 never 类型:

TypeScript:入门与进阶(一)

1.5、索引类型

索引类型指的不是某一个特定的类型工具,它其实包含三个部分:索引签名类型索引类型查询索引类型访问它们都通过索引的形式来进行类型操作,但索引签名类型是声明,后两者则是读取

1.5.1 索引签名类型

索引签名类型主要指的是在接口或类型别名中,通过以下语法来快速声明一个键值类型一致的类型结构

interface AllStringTypes {
  [key: string]: string;
}

type AllStringTypes = {
  [key: string]: string;
}

在这个例子中我们声明的键的类型为 string([key: string]),这也意味着在实现这个类型结构的变量中只能声明字符串类型的键。 但由于 JavaScript 中,对于 obj[prop] 形式的访问会将数字索引访问转换为字符串索引访问,也就是说, obj[599] 和 obj['599'] 的效果是一致的。因此,在字符串索引签名类型中我们仍然可以声明数字类型的键。类似的,symbol 类型也是如此:

interface AllStringTypes {
  [key: string]: string;
}

const foo: AllStringTypes = {
  "linbudu": "599",
  599: "linbudu",
  [Symbol("ddd")]: 'symbol',
}

索引签名类型也可以和具体的键值对类型声明并存,但这时这些具体的键值类型必须是索引签名类型的子类型

interface AllStringTypes {
  propA: number;// Error:类型“number”的属性“propA”不能赋给“string”索引类型“boolean”。
  [key: string]: boolean;
}

interface StringOrBooleanTypes {
  propA: number;
  propB: boolean;
  // 具体的键值对类型是索引签名类型
  [key: string]: number | boolean;
}

1.5.2 索引类型查询-keyof

索引类型查询,也就是 keyof 操作符,它可以将对象中的所有键转换为对应字面量类型,然后再组合成联合类型。注意,这里并不会将数字类型的键名转换为字符串类型字面量,而是仍然保持为数字类型字面量

interface Foo {
  test: 1,
  a: 2,
  599: 2
}

type FooKeys = keyof Foo; // 'test' | 'a' | 599

const fooTestType: FooKeys = 'test'
const fooAType: FooKeys = 'a'
const foo599: FooKeys = 599

const fooTrue: FooKeys = true// Error

由上可知,FooKeys是一个由字面量类型组成的联合类型('test' | 'a' | 599),可以写段伪代码来模拟  “从键名到联合类型”  的过程:

type FooKeys = Object.keys(Foo).join(" | ");

不是此联合类型的,会报错: TypeScript:入门与进阶(一)

1.5.3 索引类型访问-obj[expression]

在 JavaScript 中我们可以通过 obj[expression] 的方式来动态访问一个对象属性(即计算属性),expression 表达式会先被执行,然后使用返回值来访问属性。而 TypeScript 中我们也可以通过类似的方式,只不过这里的 expression 要换成类型:

interface NumberRecord {
  [key: string]: number;
}

type PropType = NumberRecord[string];// number

interface Foo {
  propA: number;
  propB: boolean;
}

type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean

实际上这里的'propA''propB'都是字符串字面量类型而不是一个 JavaScript 字符串值。索引类型查询的本质其实就是,通过键的字面量类型('propA')访问这个键对应的键值类型(number

也可以通过keyof 操作符一次性获取这个对象所有的键的字面量类型:

interface Foo {
  propA: number;
  propB: boolean;
  propC: string;
}

type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean

1.5.4 映射类型-in

映射类型的主要作用即是基于键名映射到键值类型

type Stringify<T> = {
  [K in keyof T]: string;
};

这个工具类型会接受一个对象类型(假设我们只会这么用),使用 keyof 获得这个对象类型的键名组成字面量联合类型,然后通过映射类型(即这里的 in 关键字)将这个联合类型的每一个成员映射出来,并将其键值类型设置为 string

当然,我们不仅可以拿到键类型,也能拿到键值类型:

type Clone<T> = {
  [K in keyof T]: T[K];
};

interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type cloneFooType = Clone<Foo>

TypeScript:入门与进阶(一) 这里的T[K]其实就是上面说到的索引类型访问,我们使用键的字面量类型访问到了键值的类型,这里就相当于克隆了一个接口。需要注意的是,这里其实只有K in 属于映射类型的语法,keyof T 属于 keyof 操作符,[K in keyof T][]属于索引签名类型,T[K]属于索引类型访问。

1.6、类型保护

在使用联合类型时,因为有类型保护措施,所以只能访问联合类型的所有类型里共有的属性或方法,如果使用的不是共有属性,则会报错。

为了解决这个报错,可使用类型断言或者类型推断去规避这个错误提示。

  • 类型断言进行类型保护,规避错误提示
interface Bird {
  fly: boolean;
  sing: () => {};
}



interface Dog {
  fly: boolean;
  bark: () => {};
}

function trainAnimal (animal: Bird|Dog) {
  if (animal.fly) {
    // 类型断言
    (animal as Bird).sing();
  } else {
    // 类型断言 
     (animal as Dog).bark();
  }
  
}
  • 使用 in语法进行类型保护
interface Bird {
  fly: boolean;
  sing: () => {};
}

interface Dog {
  fly: boolean;
  bark: () => {};
}

function trainAnimal (animal: Bird|Dog) {
  if ('sing' in animal) {
    animal.sing();
  } else {
    animal.bark();
  }
  
}
  • 使用 typeof或者 instanceof语法进行类型保护
function add(first: string|number, second: string|number) {
  // 直接拼接会报错
  // return first + second
  if (typeof first == 'string' || typeof second == 'string') {
    return `${first}${second}`
  } else {
    return first + second
  }
}

instanceof用于类型使用的是 class 类型时对类型进行判断

  • 类型推断进行类型保护
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

1.7、类型守卫-is 关键字

function isString(input: unknown): input is string {
  return typeof input === "string";
}

isString 函数称为类型守卫,在它的返回值中,我们不再使用 boolean 作为类型标注,而是使用 input is string 这么个奇怪的搭配,拆开来看它是这样的:

  • input:函数的某个参数;
  • is string:即 is 关键字 + 预期类型,即如果这个函数成功返回为 true,那么 is 关键字前这个入参的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到

其实类型守卫有些类似于类型断言,但类型守卫更宽容,也更信任你一些。你指定什么类型,它就是什么类型。  除了使用简单的原始类型以外,我们还可以在类型守卫中使用对象类型、联合类型等,比如下面开发时常用的两个守卫:

export type Falsy = false | "" | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;

export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean' , 'undefined'].includes(typeof val);

2、枚举类型(Enum)

在没有引入枚举之前的代码:

// 或是这样:
export const PageUrl = {
  Home_Page_Url: "url1",
  Setting_Page_Url: "url2",
  Share_Page_Url: "url3",
}

如果把这段代码替换为枚举,会是如下的形式:

enum PageUrl {
  Home_Page_Url = "url1",
  Setting_Page_Url = "url2",
  Share_Page_Url = "url3",
}

const home = PageUrl.Home_Page_Url;

枚举和对象的重要差异在于,对象是单向映射的,我们只能从键映射到键值。而枚举是双向映射的,即你可以从枚举成员映射到枚举值,也可以从枚举值映射到枚举成员。

enum Items {
  Foo,
  Bar,
  Baz
}

const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"

以上的枚举会被编译为以下 JavaScript 代码:

var Items;
(function (Items) {
    Items[Items["Foo"] = 0] = "Foo";
    Items[Items["Bar"] = 1] = "Bar";
    Items[Items["Baz"] = 2] = "Baz";
})(Items || (Items = {}));
var fooValue = Items.Foo; // 0
var fooKey = Items[0]; // "Foo"

Items[k] = v 的返回值即是 v,因此这里的 Items[Items[k] = v] = k 本质上就是进行了 Items[k] = vItems[v] = k 这样两次赋值。

但需要注意的是,仅有值为数字的枚举成员才能够进行这样的双向枚举字符串枚举成员仍然只会进行单次映射

enum Items {
  Foo,
  Bar = "BarValue",
  Baz = "BazValue"
}

以上的枚举会被编译为以下 JavaScript 代码:

var Items;
(function (Items) {
    Items[Items["Foo"] = 0] = "Foo";
    Items["Bar"] = "BarValue";
    Items["Baz"] = "BazValue";
})(Items || (Items = {}));

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射。这些常量被真正地约束在一个命名空间下。

enum Status {
  OFFLINE,
  ONLINE,
  DELETED
}

console.log(Status.OFFLINE, Status[0]);// 0  OFFLINE

console.log(Status.DELETED, Status[2]);// 2  DELETED

也可以给枚举项手动赋值初始值,未手动赋值的枚举项会接着上一个枚举项递增。

enum Status {
  OFFLINE=1,
  ONLINE,
  DELETED
}

console.log(Status.OFFLINE, Status[0]);// 1  OFFLINE

console.log(Status.DELETED, Status[2]);// 3  DELETED

在数字型枚举中,你可以使用延迟求值的枚举值,比如函数:

const returnNum = () => 100 + 499;

enum Items {
  Foo = returnNum(),
  Bar = 599,
  Baz
}

console.log(Items.Foo) // 599

但要注意,延迟求值的枚举值是有条件的。如果你使用了延迟求值,那么没有使用延迟求值的枚举成员必须放在使用常量枚举值声明的成员之后(如上例),或者放在第一位

3、常量枚举

常量枚举和枚举相似,只是其声明多了一个 const

const enum Items {
  Foo,
  Bar,
  Baz
}

const fooValue = Items.Foo; // 0

它和普通枚举的差异主要在访问性与编译产物。对于常量枚举,你只能通过枚举成员访问枚举值(而不能通过值访问成员)。

TypeScript:入门与进阶(一) 同时,在编译产物中并不会存在一个额外的辅助对象(如上面的 Items 对象),对枚举成员的访问会被直接替换为枚举的值。以上的代码会被编译为如下形式:

var fooValue = 0 /* Items.Foo */; // 0

拓展:定义一个number类型,值在4-400的范围之间

TypeScript:入门与进阶(一) Enumerate类型是一个递归类型,它用来创建一个长度为 N 的数字数组。它的第一个参数 N 是数字类型,表示数组的长度。它的第二个参数 Acc 是数字数组类型,表示当前递归中数组的值。当 Acc 的长度等于 N 时,这个递归类型会停止递归,并返回 Acc 数组中的每一项。如果 Acc 的长度小于 N,那么递归类型会将当前的长度添加到 Acc 数组中,并继续递归。IntRange 是一个由两个数字类型参数 F 和 T 组成的类型,它表示从 F 到 T 的一个整数范围。它通过使用 Exclude 类型来排除由 Enumerate 类型生成的数组中的所有小于 F 的元素,从而实现F到T

4、泛型(generic)

4.1、定义

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

// 定义时参数类型不定义
// 在函数名后添加了 `<T>`,其中 `T` 用来指代任意输入的类型,函数参数类型就是T
function join<T>(first: T, second: T) {
  return `${first}${second}`
}

// 使用时  T 指定类型为string
join<string>('a', 'b');

// 使用时再指定类型为number
join<number>(1, 1);


// 函数参数类型就是T类型的数组
function map<T>(params: T[]) {
  return params;
}

map<string>(['a'])

4.2、多个类型参数

function join<T, P>(first: T, second: P) {
  return `${first}${second}`;
}

// join<number, string>(1, '1');

// 类型推断出类型
join(1, '1')

// 函数参数和返回值都可以使用泛型
function anotherJoin<T>(first: T, second: T): T {
  return first;
}

4.2、类中的泛型

  • 类里面也可以使用泛型。
class DataManage<T> {
  constructor(private data: T[]) {}
  getItem(index: number) : T {
    return this.data[index]
  }
 }

 const data = new DataManage<number>([1])

console.log(data.getItem(0));;
  • 类的泛型还可以继承接口
interface Item {
  name: string
}

// T 这个泛型继承了 Item,并且以后,必须有Item对应的属性
class DataManage<T extends Item> {
  constructor(private data: T[]) {}
  // 返回值应该是跟Item的name属性保持一致
  getItem(index: number):string {
    return this.data[index].name
  }
 }

//  const data = new DataManage<number>([1])

// console.log(data.getItem(0));;

const data = new DataManage([
  { name: 'dell' }
])
  • 类的泛型还可以继承联合类型
class DataManage<T extends string | number> {
  constructor(private data: T[]) {}
  getItem(index: number): T {
    return this.data[index]
  }
 }

 const data = new DataManage<number>([1]);
 console.log(data);

4.4、泛型类型

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

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

4.5、泛型分类

4.5.1、类型别名中的泛型

type Factory<T> = T | number | string;

类型别名的本质就是一个函数,T 就是它的变量,返回值是一个包含 T 的联合类型。

类型别名中的泛型大多是用来进行工具类型封装:

type Stringify<T> = {
  [K in keyof T]: string;
};

type Clone<T> = {
  [K in keyof T]: T[K];
};

工具类型 Partial 的内部实现就是如此,但会额外添加一个?

// Partial实现
type Partial<T> = {
  [P in keyof T]?: T[P];
};


interface IFoo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type PartialIFoo = Partial<IFoo>;

// 等价于
interface PartialIFoo {
  prop1?: string;
  prop2?: number;
  prop3?: boolean;
  prop4?: () => void;
}

4.5.2、条件类型

类型别名与泛型的结合中,除了映射类型、索引类型等类型工具以外,还有一个非常重要的工具:条件类型。在条件类型参与的情况下,通常泛型会被作为条件类型中的判断条件(T extends Condition,或者 Type extends T)以及返回值(即 : 两端的值)。

type IsEqual<T> = T extends true ? 1 : 2;

type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'linbudu'>; // 2

4.6、泛型默认值

像函数可以声明一个参数的默认值一样,泛型同样有着默认值的设定,默认会使用我们声明的默认值来填充。

type Factory<T = boolean> = T | number | string;
const foo: Factory = false;

4.7、多泛型关联

多泛型参数其实就像接受更多参数的函数,其内部的运行逻辑(类型操作)会更加抽象,表现在参数(泛型参数)需要进行的逻辑运算(类型操作)会更加复杂。

type Conditional<Type, Condition, TruthyResult, FalsyResult> =
  Type extends Condition ? TruthyResult : FalsyResult;

//  "passed!"
type Result1 = Conditional<'linbudu', string, 'passed!', 'rejected!'>;

// "rejected!"
type Result2 = Conditional<'linbudu', boolean, 'passed!', 'rejected!'>;

5、typescript中的keyof、in

5.1、keyof+泛型

keyof 与 Object.keys 略有相似,只不过 keyof 取 interface 的键。

当我们需要获取对象里的值的时候,我们可能这么写:

interface Person {
  name: string;
  age: number;
  gender: string;
}

class Teacher {
  constructor (private info: Person) {}
  getInfo(key: string) {
    return this.info[key]
  }
}

const teacher = new Teacher({
  name: 'dell',
  age: 18,
  gender: 'male'
})

const test = teacher.getInfo('name')
console.log(test);

从上图可以看出,getInfo的返回值是any,无法确定返回值类型,丧失了ts的优势。 TypeScript:入门与进阶(一)

同时,也无法对key进行约束,如果传入的不是Person接口里对应的属性,编译器也不会提示报错

// 传入的参数不是 Person接口里的属性 也不会报错
const test1 = teacher.getInfo('hello')
console.log(test1);

这时,就可以用 keyof泛型 来增强 getInfo 函数的类型功能:

interface Person {
  name: string;
  age: number;
  gender: string;
}

class Teacher {
  constructor (private info: Person) {}
  // getInfo(key: string) {
  //   return this.info[key]
  // }

  getInfo<T extends keyof Person>(key: T): Person[T] {
    return this.info[key];
  }
}

// 编译器会提示错误
const test1 = teacher.getInfo('hello')
console.log(test1);

可以看出,编译器对key进行的约束 TypeScript:入门与进阶(一)

同时,函数的返回值也有确定的类型 TypeScript:入门与进阶(一)

更多用法可参考知乎的文章

5.2、in

in用于取联合类型的值。主要用于数组和对象的构造。

type name = 'firstName' | 'lastName';

type TName = {
  [key in name] : string
}

TypeScript:入门与进阶(一)

6、声明文件

因为在typescript中,引入第三方插件时,无法识别,所以需要声明文件,才能获得对应的代码提示等功能。

声明文件的格式是[name].d.ts,必须以.d.ts结尾。

第三方库有专门的社区管理,也可以在这个页面搜索需要的声明文件。