TypeScript实用笔记

505

概述

TypeScript 是 JavaScript 的超集(扩展集): 包含 JavaScript / 类型系统 / ES6+;它最终编译成为 Javascript;

优点:

  • 任何一种 JavaScript 运行环境都支持 TypeScript;
  • TypeScript 相较于 Flow 更能更加强大,生态也更健全、更完善;
  • TypeScript 可以在编写时时提醒类型错误,不需要额外配合命令或者插件去检查;

缺点:

  • 语言本身多了很多概念: 接口/范型/枚举等...,增加学习上手成本;
  • 项目初期, TypeScript 会增加一些成本, 例如需要定义很多:类、模型、接口之类的声明;

快速上手

安装:

// 初始化package
yarn init --yes

// 安装TypeScript 依赖
yarn add  typescript --dev

// TypeScript 可以完全按照 JavaScript 标准语法编写代码;

const helloTypeScript = (name: Number) => {
  console.log('hello' + name)
}

helloTypeScript('world!!') // error 类型“string”的参数不能赋给类型“Number”的参数。

helloTypeScript(100) // success

配置文件

命令行配置:

yarn tsc --init           // Successfully created a tsconfig.json file

tsconfig.json:

// 具体看配置文件
{
  "compilerOptions": {
    "target": "es5",       //  按ES5规则编译文件
    "module": "commonjs",  //  按commonjs规范输出 
    "sourceMap": true,
    "outDir": "dist",     // 输出文件夹
    "rootDir": "./",      // 当前目录下的ts文件
    "strict": true,       //  严格模式下,不允许出现未声明类型
  }
}

运行命令:

yarn tsc

原始类型

const a: string = 'test'

const b: number = 100

const c: boolean = true

// 非严格模式下 "strict": false,string | number | boolean 类型下可以为null 或者 undefined
const d: boolean = undefined

const e: void = undefined

const f: null = null

const g: undefined = undefined

标准库声明

标准库就是内置对象所对应的声明文件。

tsconfig.json 中声明了标准库为 "target": "es5", 所以,当程序中使用 ES6 中的对象,例如:Symbol 类型:

const h:symbol = Symbol()
// error: "Symbol" 仅指类型,但在此处用作值。是否需要更改目标库?请尝试将 `lib` 编译器选项更改为 es2015
// 或更高版本。

这种时候,我们需要更改 tsconfig.json 中的 target 为: "target: "es2015"; 或者 添加lib

"lib": ["ES2015"],

设置中文错误消息

VS Code 设置 => 搜索 => typescript locale 切换语言; 默认为 null,跟谁编辑器系统设置

Object 类型

Typescript 中的类型不单指普通对象类型,而是泛指数组、对象、函数类型;定义普通对象类型 用 {}:

// object 对象类型
const foo: object = {} || [] || () => {} // 成立

// {} 对象类型 可以使用对象字面量的方式, 更优的方式适用接口来定义对象
const foo: { name: string, age: number } = { foo: 123, bar: 'string' }

Array 类型

  • 泛型: Array<T>
  • 元素类型:T[]
const arr1: Array<number> = [ 1, 2, 3 ]

const arr2: number[] = [ 1, 2, 3 ]

// 一个实例
const sum = (...args: number[]): number => args.reduce((prev, current) => prev, current, 0)

console.log(sum(1, 2, 3, 'a')) // args TypeError

元组类型

元组就是一个明确数量并且明确每个元素类型的数组:

const tuple: [number, string] = [18, 'age'] // true

// Type '[number, string, number]' is not assignable to type '[number, string]'.Source has 3 element(s) // but target allows only 2
const tuple: [number, string] = [18, 'age', 20]

// Type 'number' is not assignable to type 'string'.
const tuple: [number, string] = [18, 20]

用途: 元组一般可以用来在一个函数中返回多个返回值;

枚举类型

某几个数值代表某种的状态,它有两个特点:

  • 给一组数值起一个更好理解的名字;
  • 一个枚举中只会存在几个固定值,不会存在超出范围的可能性;

传统定义变量:

const activeStatus = {
  START: 1,
  FINISHED: 2,
  ENDING: 3
}

const activeInfo = {
  id: 10000,
  name: '一个活动',
  status: 1
}

activeInfo.status === ActiveStatus.START // 活动已开始
activeInfo.status === ActiveStatus.FINISHED // 活动已完成
activeInfo.status === ActiveStatus.ENDING // 活动已结束

枚举类型定义:

enum ActiveStatus {
  START = 0,
  FINISHED,
  ENDING
}

const activeInfo = {
  id: 10000,
  name: '一个活动',
  status: 1
}

console.log(activeInfo.status === ActiveStatus.START) // false
console.log(activeInfo.status === ActiveStatus.FINISHED) // true

枚举值会编译到编译后的代码中,影响编译后的结果, 编译结果如下:

var ActiveStatus;
(function (ActiveStatus) {
  ActiveStatus[ActiveStatus["START"] = 0] = "START";
  ActiveStatus[ActiveStatus["FINISHED"] = 1] = "FINISHED";
  ActiveStatus[ActiveStatus["ENDING"] = 2] = "ENDING";
})(ActiveStatus || (ActiveStatus = {}));

var activeInfo = {
  id: 10000,
  name: '一个活动',
  status: 1
};

console.log(activeInfo.status === ActiveStatus.FINISHED);

常量枚举,编译过后不会侵入编译代码,而是会以注释的方式:

// 编译前:
const enum ActiveStatus {
  START = 0,
  FINISHED,
  ENDING
}

console.log(activeInfo.status === ActiveStatus.START);
console.log(activeInfo.status === ActiveStatus.FINISHED);

// 编译后
var activeInfo = {
  id: 10000,
  name: '一个活动',
  status: 1
};

console.log(activeInfo.status === 0 /* START */); // false
console.log(activeInfo.status === 1 /* FINISHED */); // true

函数类型

函数的类型约束主要是对函数的输入(参数)和输出(返回值)进行类型限制:

JavaScript   有两种函数的定义方式:

  • 函数声明
// 必传参数限制:
function fun1(a: number, b: number): string {
  return 'hellow' + a + b
}

fun1(10, 10) // true

fun1(10) // false

fun1(100, 10,10) // false

// 可选参数1
function fun2(a: number, b: number, c?: string): string {
  return 'hellow' + a + b + c
}

fun2(100, 10) // true

fun2(100, 10, 'world') // true

fun2(100, 10, 10) // false

// 可选参数2-ES6设置默认值语法
function fun3(a: number, b: number, c: string = 'world'): string {
  return 'hellow' + a + b + c
}

设置可选参数时,不管? 语法 还是默认值语法,都必须放在最后; 当存在多个可选参数时,须从最后开始依次设置;

  • 函数表达式

需要定义函数输入和输出的类型:

const fun3: (a: number, b: number, c?: string, d?: string) => string = function (a: number, b: number, c: string = 'world') {
  return 'hellow' + a + b + c
}

类型断言

在某些特定情况下,TypeScript 无法断言某些变量的具体类型,但是作为开发者能够明确知道变量类型的情况下,就可以使用类型断言来告诉 TypeScript 定义的变量将是什么类型;

例如:服务端返回了一组数据表示学生的年级信息,现在需要筛选出特定年级学生:

const data = [100, 200, 300]

// 定义一个变量
const findRst = data.find(res => res > 1) // filter: number || undefined

// 断言 用as关键词
const assertFilter = findRst as number

// 断言 用<>
const assertFilter2 = <number>findRst // JSX 下<> 会与标签产生冲突,不建议使用

注意:类型断言只是辅助 TypeScript 进行类型推断的一种方式,它并不是类型转换; 类型转换是运行阶段的一个概念,类型断言只出现在编辑器编译阶段;

接口 Interfaces | type

Interfaces 可以理解为一种规范或者契约,他是一种抽象的概念,他可以用来去约束对象的结构,我们使用一个接口就必须要遵循这个接口全部的约定;

TypeScript 中,接口最直观的体现就是可以用来约定在对象属性中具体需要定义哪些成员;而且这些成员类型是怎样的;

// 定义接口
interface Post {
  title: string // 可加; 或者不可加
  content: string
}

// 约束参数类型
function printPost(post: Post) {
  console.log(post.title)

  console.log(post.content)
}

// 使用,少传/传错都会引起类型错误
printPost({
  title: 'name',
  content: 'hello'
})

特殊用法:设置可选成员 | 只读成语 | 多类型成员 | 动态类型

interface Post {
  title: string
  content: string
  desc?: string // 可选成员
  readonly summary: string // 只读成员, 设置之后不可更改
  id: string | number // 多类型
  [prop: string]: any // 动态属性,用于兼容未定义属性
}

const postObj = {
  title: 'name',
  content: 'hello'
}

接口的另一种定义方式: type

参考

interfacetype 的区别:

相同点:

1、都可以描述一个对象或者函数:

// interface
interface User {
    name: string
    age: number
}

interface SetUser {
    (name: string, age: number): void;
}
// type
type User = {
    name: string
    age: number
};
type SetUser = (name: string, age: number): void;

2、都允许拓展(extends) interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。 虽然效果差不多,但是两者语法不同。

// interface extends interface
interface Name { 
 name: string; 
}
interface User extends Name { 
 age: number; 
}

// type extends type
type Name = { 
 name: string; 
}
type User = Name & { age: number };

// interface extends type
type Name = { 
 name: string; 
}
interface User extends Name { 
 age: number; 
}

// type extends interface
interface Name { 
 name: string; 
}
type User = Name & { 
 age: number; 
}

不同点: type 可以而 interface 不行 type 可以声明基本类型别名,联合类型,元组等类型

// 基本类型别名
type Name = string

// 联合类型
interface Dog {
 wong();
}
interface Cat {
 miao();
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]

type 语句中还可以使用 typeof 获取实例的 类型进行赋值.

// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div

其他骚操作:

type StringOrNumber = string | number; 
type Text = string | { text: string }; 
type NameLookup = Dictionary<string, Person>; 
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> };

interface 可以而 type 不行: 1、interface 能够声明合并

interface User {
 name: string
 age: number
}

interface User {
 sex: string
}

/*
User 接口为 {
 name: string
 age: number
 sex: string 
}
*/

类的基本使用

类用来描述具体事务的抽象特征;用来描述一类具体对象的抽象成员;

TypeScript 增强了 class 的相关语法,与 ES6 定义类有些许不同的是, TypeScript 中,类的属性不能直接在构造函数中定义, 而是需要先声明,指定类型,然后才能在构造函数中初始化;

例如:

class Person {
  name: string
  age: string

  constructor (name: string, age: number) {
    this.name = name
    this.age = age
  }
}

特殊用法-访问修饰符 private: 私有的 | public: 公开的protected: 受保护的,其中:

  • private: 定义的属性只能在 class 内部访问, 无法通过实例获取到
  • public: class 中的属性默认是 public
  • protected: 无法通过实例获取到, 与private 的区别在于: 除了在 class 内部访问 还能在子类中获取到:

例如:

class Person {
  name: string
  age: number
  protected gender: boolean

  constructor (name: string, age: number) {
    this.name = name
    this.age = age
    this.gender = true
  }
}

class Children extends Person {
  constructor(name: string, age: number) {
    super(name, age)
    console.log(this.gender) // true
  }
}

特殊说明: constructor 设置为 private 时, 该 class 不可被实例化,也不可被继承。该类的实例化只能通过定义静态方法,在内部被实例化:

class Student extends Children {
  static of(name: string, age: number) {
    return new Student(name, age)
  }

  private constructor(name: string, age: number) {
    super(name, age)
    console.log(this.gender) // true
  }
}

const newStudent = Student.of('lelei', 9) // true

类的只读属性: readonly。当类的属性已经具备访问修饰符,那么 readonly 只能跟在属性修饰符后面:

class Student extends Children {
  public readonly name: string
  protected readonly age: number
}

类与接口

不同的类与类之间有一些共同的特征。对于这些公共的特征,我们一般使用接口去抽象:

实例演示:

class Person {
  eat(food: string): void {
    console.log('为了享受美食的乐趣,吃:' + food)
  }

  run(distance: number): void {
    console.log('为了直立行走:'+ distance)
  }
}

class Animal {
  eat(food: string): void {
    console.log('为了填饱肚子,吃:' + food)
  }

  run(distance: number): void {
    console.log('爬行'+ distance)
  }
}

上面,定义了两个类 Person 与 Animal, 他们都具有能吃能跑的能力,但是他们的能力实现是不一样的。 通常我们将为拥有同样能力且能力实现也一样的多个类,用定义父类继承的方式来实现能力的继承和约束。 而将为拥有同样能力但能力实现不一样的类,我们通过定义接口的方式约束多个类型之间公共的能力。

定义接口只是为了约束函数类型,具体不做方法的实现,同时最佳实践是一个接口只约束一种能力:

interface Eat {
  eat(food: string): void
}

interface Run {
  run(distance: number): void
}

实现接口,使用 implements 关键字

class Persons implements Eat, Run{
  eat(food: string): void {
    console.log('为了享受美食的乐趣,吃:' + food)
  }

  run(distance: number): void {
    console.log('为了直立行走:'+ distance)
  }
}

class Animal implements Eat, Run{
  eat(food: string): void {
    console.log('为了填饱肚子,吃:' + food)
  }

  run(distance: number): void {
    console.log('爬行'+ distance)
  }
}

抽象类

抽象类在某种程度上来说跟接口类似,用来约束摸某个子类中必须具备某种成员。不同于接口的是,抽象类可以包含具体的实现,而接口只是成员的抽象,不包含成员具体的实现。

一般比较大的类(只是一种大的泛指,而不是具体的细分类型),使用抽象类:

定义抽象类的方式: 使用abstract 关键字,定义在类之前。定义成抽象类之后,类只能被继承, 不能被实例化;要实例化抽象类,则必须定义子类继承父类,然后实例化子类。

// 抽象类
abstract class Animal {
  // 可以包含具体方法的实现
  eat(food: string): void {
    console.log(`咩咩咩, ${food}`)
  }

  // 可以定义抽象方法,用于约束子类的同类方法,  抽象方法不需要方法体
  abstract run(distance: number) : void
}

// 子类
class Dog extends Animal {
  run(distance: number): void {
    // throw new Error("Method not implemented.")
    console.log(`登登登的跑, ${distance}`)
  }
}

// 实例化
const smallDog = new Dog()

console.log(smallDog.eat('鱼'))

console.log(smallDog.run(100))

泛型

泛型: 定义函数或者接口或类的时候,不去指定具体的类型,只有在使用的时候才指定类型的一种实现。

目的:极大程度的复用代码;

实例:

// 数字数组
function createNumberArray(length: number, value: number): number[] {
  return Array<number>(length).fill(value)
}

// 字符串数组
function createStringArray(length: number, value: string): string[] {
  return Array<string>(length).fill(value)
}

泛型实现:用关键字T来指定那些类类型不确定的参数, 例如:

// 将不确定的类型用T代替
function createArray<T> (length: number, value: T): T[] {
  return Array<T>(length).fill(value)
}

// function createArray(length: number, value: unknown): unknown[]
createArray()

// function createArray<string>(length: number, value: string): string[]
createArray<string>(5, 'test')

// function createArray(length: number, value: unknown): unknown[]
createArray<boolean>(5, true)

接口:

interface ParsedQuery<T = string> {
	[key: string]: T | T[] | null;
}

总的来说范型就是把我们定义时不能明确的类型变成一个参数,然后使用的时候在来指定和传递此参数。

类型声明

当一个函数/接口,在定义时未声明其类型,而使用时想要声明类型时,就可以使用关键字declare类型声明语句:

declare function doSomething(prams: Type): Type

在使用一些第三方插件,如果插件未支持 TypeScript 时,可以尝试安装支持 TypeScript 版本:@type/xxx

传送门