ts笔记

1,583 阅读12分钟

背景

typescript 是微软发明的,像js是网景公司发明的, 
每一门语言都是人发明的,由于是人发明的,人总是会犯错, 
所以每一门语言都会有一些错误,那typescript主要就是为了解决js的一些错误或认为它不好的地方

TS是什么

TypeScript = Type + Script(标准JS)。 我们从TS的官方网站上就能看到定义:

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript。

TypeScript是一个编译到纯JS的有类型定义的JS超集

JavaScript 和 TypeScript 的主要差异

TypeScript 可以使用 JavaScript 中的所有代码和编码概念,TypeScript 是为了使 JavaScript 的开发变得更加容易而创建的。例如,TypeScript 使用类型和接口等概念来描述正在使用的数据,这使开发人员能够快速检测错误并调试应用程序

  • TypeScript 从核心语言方面和类概念的模塑方面对 JavaScript 对象模型进行扩展。
  • JavaScript 代码可以在无需任何修改的情况下与 TypeScript 一同工作,同时可以使用编译器将 TypeScript 代码转换为 JavaScript。
  • TypeScript 通过类型注解提供编译时的静态类型检查。
  • TypeScript 中的数据要求带有明确的类型,JavaScript不要求。
  • TypeScript 为函数提供了缺省参数值。
  • TypeScript 引入了 JavaScript 中没有的“类”概念。
  • TypeScript 中引入了模块的概念,可以把声明、数据、函数和类封装在模块中。

安装与调试

npm install typescript@2.9.2 -g

安装完之后我们会得到两个命令tsc和tsserver

tsc: TypeScript Compiler 把ts变成js的程序

npm install ts-node@7.0.0 -g

让node支持typescript

注意记下 ts-node 安装后的可执行文件路径,后面要用的

为什么用TS

从开发效率上看,

虽然需要多写一些类型定义代码,但TS在VSCode、WebStorm等IDE下可以做到智能提示,智能感知bug,同时我们项目常用的一些第三方类库框架都有TS类型声明

解决了 IDE/编辑器无法智能提示的痛点。

使用第三方库时方便查看文档(使用 TS 的过程就是一种学习的过程)
  • 老是将数组的slice和splice方法搞混
  • 使用Uniapp的 弹框组件,配合idea快速了解api需要哪些参数,每个参数是什么类型, 且参数代表什么含义

配合IDE/编辑器 智能感知bug
  • 声明变量时需要指定类型

  • 接口规定了对象中要包含的属性(不能多也不能少)

  • enum枚举类型的属性 只读

  • 拼写错误

从可维护性上看

长期迭代维护的项目开发和维护的成员会有很多,团队成员水平会有差异,而软件具有熵的特质,长期迭代维护的项目总会遇到可维护性逐渐降低的问题,有了强类型约束和静态检查,以及智能IDE的帮助下,可以降低软件腐化的速度,提升可维护性,且在重构时,强类型和静态类型检查会帮上大忙,甚至有了类型定义,会不经意间增加重构的频率(更安全、放心)

从线上运行时质量上看

现在的SPA项目的很多bug都是由于一些调用方和被调用方(如组件模块间的协作、接口或函数的调用)的数据格式不匹配引起的,由于TS有编译期的静态检查,让我们的bug尽可能消灭在编译器,加上IDE有智能纠错,编码时就能提前感知bug的存在,我们的线上运行时质量会更为稳定可控

TS能干点啥

类型系统

js七种数据类型 + any + enum + void + never

let isDone: boolean = false;

let decimal: number = 6;

let color: string = "blue";

// 数组,有两种写法
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];

// 元组(Tuple)各元素的类型不必相同
let x: [string, number] = ["hello", 10];

// 枚举
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

// 不确定的可以先声明为any
let notSure: any = 4;

// 声明没有返回值
function warnUser(): void {
    alert("This is my warning message");
}

let u: undefined = undefined;

let n: null = null;

// 类型永远没返回
function error(message: string): never {
    throw new Error(message);
}

// 程序员自身 主观判断 h是一个字符串(这个判断不一定对),ts会相信你,如果程序报错就是程序员的锅
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;
类型编译检查
  • 运行时报错: 浏览器读取html,遇到 script标签,下载并运行js,bug只会在运行时发现
  • 编译时报错: 在编译的时候报错,提前处理bug, 而不是当用户执行脚本时才报错
    • .java --javac--->.class字节码文件, jvm运行.class文件
    • .ts --tsc --->.js文件, 浏览器运行

类型推断

让我们既能享受js的灵活又能享受ts的类型检查

// 如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
let myFavoriteNumber = 'seven'; 
//等价于
let myFavoriteNumber :string= 'seven';

面向对象编程增强

面向对象(oop)的三大特征: 封装,继承,多态(抽象类, 接口, 泛型)

封装: 隐藏内部实现细节,对外只暴露接口,调用方只需按接口规定调用即可,封装的重要手段就是访问控制权限,在ts中为 public/protected/private

继承:子类继承父类,子类拥有父类中除了private声明的所有属性(创建子类时调用父类的构造方法)

多态: 多态是由于继承或实现而引出的,继承者可以重写父类的方法, 实现者可以实现接口中的抽象方法, 当用父类的引用指向子类的对象时,就形成了向上造型,从而形成了多态

访问权限控制

TS中的类比js中的类多了 抽象类和访问权限限制 两个概念

JS面向对象编程的一个大问题就是没有提供原生支持信息隐藏的方案(很多时候都是通过约定编码方式来做)

私有的成员变量,公共的get/set方法

class Demo7 {
   private _name:string;
   private _age:number;
   
   constructor(name: string, age: number) {
      this._name = name;
      this._age = age;
   }
   
   get name(): string {
      return this._name;
   }

   set name(value: string) {
      this._name = value;
   }

   get age(): number {
      return this._age;
   }

   set age(value: number) {
      this._age = value;
   }
}

接口

TS中的一个核心原则之一就是类型检查关注的是值的形状,有时就叫做“鸭子辨型”(duck typing)或“结构化子类型”(structural subtyping)。TS中interface就承担了这样的角色,定义形状与约束,在内部使用或者和外部系统协作

接口在定义的时候,不能初始化属性以及方法,属性不能进行初始化,方法不能实现方法体。 类实现接口之后,必须声明接口中定义的属性以及方法。

/**
 * 接口就是用代码描述一个对象必须包含什么属性(包括方法), 不能多也不能少
 */
interface Shape {
  head: string,
  body: string
}

interface Human {
    name: string,
    age: number,
    shape: Shape,
    say(word:string): void;
}

interface Man extends Human3{
    gender:string
}

let h: Man = {
    name: '张三',
    gender: '男',
    age: 27,
    shape: {head: '头', body: '身体'},
    say(word: string): void {
        console.log(word)
    }
}


  // 可选属性
  interface SquareConfig {
    color?: string;
    width?: number;
  }
  
  // 只读属性
  interface Point {
    readonly x: number;
    readonly y: number;
  }

类实现接口之后,必须声明接口中定义的属性以及方法,

interface Animal11 {
    name: string;

    eat: () => void;
}
class Dog implements Animal11 {

    name: string = '小白';

    eat() {
        console.log('tag', 'I love eat bone!')
    }
}
const dog: Dog = new Dog();
dog.eat();

数据库中的一条记录对应java中的一个实例对象,该类中的字段和字段类型与表中的字段及字段类型匹配, 有了TS的接口后,我们可以用 接口和 后端的实体类(或接口返回的数据) 一一对应,这样就不会出现接口返回属性找不到,或接口返回数据类型不匹配的问题

抽象类

/**
 * Abstract class 抽象类(class能够拥有的 abstract class 都拥有)
 * 
 *  抽象类中可能包含抽象方法(只有方法名,没有具体实现的方法),所以抽象类不可以实例化, 一般都作为父类
 *  反证法: 抽象类如果可以实例化,实例化出来的对象如何执行抽象方法(没有方法)
 *
 *  class --> abstract class --> interface
 *  抽象程度逐渐提升,interface 抽象程度最高
 */
abstract class Animal {
   kind:string;

    constructor(kind: string) {
        this.kind = kind;
    }

    breath():void {
        console.log('呼吸')
    }

    // Method 'birthType' cannot have an implementation because it is marked abstract
    // 抽象方法不能有具体实现
    abstract birthType():void
}

class Human1 extends Animal {
    // 子类实现抽象父类中的抽象方法
    birthType(): void {
        console.log('胎生')
    }
}

泛型

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件

总结一句话: 当我们需要支持多种类型时,用T当占位符,这个T就是泛型,代表广泛的类型

/**
 * 泛型就是用一个东西表示广泛的类型。可以理解为一个占位符
 */
function returnIt<T>(arg: T): T{
    return arg;
}

let s = returnIt<string>('hi')
let n = returnIt<number>(123)

/**
 * 泛型接口
 */
 interface genFn<T> {
    ( arg : T ) : T;
};
let ide : genFn<number> = identity;

/**
 * 泛型类
 */
class Gen <T> {
    zero : T;
    add : ( x : T, y : T ) => T;
}
let gen = new Gen<Number>();

/**
 * 泛型约束: 就是给泛型添加一些约束
 *  例如: 并不是任意的类型T的实例 都具备length属性, 我们先创建一个接口Lengthwise, 则T extends Lengthwise 时, T一定具备length属性
 */
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

T vs any

// T代表广泛的类型, 参数是T类型,返回值是T类型,一一对应
function f1<T>(arg:T):T {
    return arg
}
let s1 = f1('hello')
s1.length;

// 如果使用any的话 无法做类型推断
function f2(arg:any):any {
    return arg
}
let s2 = f2('hello')

其实数组接受的就是一个泛型

let ss:Array<string> = ['hello']

函数

/**
 *你会发现用ts写函数会很放心,因为我 入参类型写死了,返回值类型也写死了,唯一有可能出错的地方就是我的逻辑代码
 * 也方便重构,n年之后回头再看这个函数,一目了然, 如果用js的话,你可能要去差这个函数到底返回什么类型的值
 */
function add(a:number, b:number):number {
    return a + b
}

可选参数

/**
 * TypeScript里的每个函数参数都是必须的,而编译器检查用户是否为每个参数都传入了值
 * JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 *
 * 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能
 */
function hi2(name:string, age?:number) {
  console.log(`Hi, ${name}, ${age}`)
}
hi2('张三', 27)

重载

/**
 *  重载: 函数重载的意义在于能够让你知道传入不同的参数得到不同的结果
 *
 *  关于函数重载,必须要把精确的定义放在前面,
 *  最后函数实现时,需要使用 |操作符或者?操作符,把所有可能的输入类型全部包含进去
 */

// 上边是声明
function add1 (arg1: string, arg2: string): string;
function add1 (arg1: number, arg2: number): number;
function add1 (arg1: string | number, arg2: string | number) {
    // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2
    if (typeof arg1 === 'string' && typeof arg2 === 'string') {
        return arg1 + arg2
    } else if (typeof arg1 === 'number' && typeof arg2 === 'number') {
        return arg1 + arg2
    }
}

// 参数要么同时为 number 要么同时为 string, 因为只定义了这两种
// add1(1, '1')

重载和重写有什么区别?

类型声明文件

由于非常多的JavaScript库并没有提供自己关于TypeScript的声明文件,导致TypeScript的使用者无法享受这种库带来的类型,因此社区中就出现了一个项目DefinitelyTyped,他定义了目前市面上绝大多数的JavaScript库的声明,当人们下载JavaScript库相关的@types声明时,就可以享受此库相关的类型定义了

TypeScript有一个叫做声明文件的功能, 即一个以.d.ts为扩展名的文件,通过它我们可以为JavaScript文件定义类型

.d.ts文件。 方法里没有实现细节,它只描述了类型。 TypeScript将.d.ts 和 .js组合到一起,这样,在编译是使用这个声明文件,在运行时使用这个原生的JS文件, 这样我们就可以 使用Ts提供的类型安全和idea的智能语法提示

学习成本

老项目迁移

对于老项目,由于TS兼容ES规范,所以可以比较方便的升级现有的JS(这里指ES6及以上)代码,逐渐的加类型注解,渐进式增强代码健壮性。迁移过程:

  • npm全局安装typescript包,并在工程根目录运行tsc --init,自动产生tsconfig.json文件。

    • 默认的3个配置项:更多配置项说明
      • "target":"es5": 编译后代码的ES版本,还有es3,es2105等选项。
      • "module":"commonjs":编译后代码的模块化组织方式,还有amd,umd,es2015等选项。
      • "strict":true:严格校验,包含不能有没意义的any,null校验等选项。
  • 初始化得到的tsconfig.json无需修改,增加"allowJs": true选项。

  • 配置webpack配置,增加ts的loader,如awesome-typescript-loader。

loaders: [
	// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
	{ test: /\.tsx?$/, loader: "awesome-typescript-loader" }
]

此时你可以写文件名为ts和tsx(React)后缀的代码了,它可以和现有的ES6代码共存,VSCode会自动校验这部分代码,webpack打包也没问题了。

  • 逐渐的,开始打算重构以前的ES6代码为TS代码,只需将文件后缀改成ts(x)就行,就可以享受TS及IDE智能感知/纠错带来的好处。