11月03, 2018

typescript入门

做个笔记。

前言

为什么突然之间学这个了。其实我也不想,但现在越来越多的框架都使用了ts来写代码。虽然勉强能看懂一些,但还是希望整个系统地学习一下。

编译工具

tsc

这个只要全局安装了typescript,就能在命令行出来。

alt

当然也可以不装全局:

npm install typescript -D
// package.json
{
    "scripts": {
         "dev": "tsc -w"
    }
}

配置文件

babel一样,ts也有一个自己的配置文件:tsconfig.json,可以通过tsc --init生成。

alt

tsconfig.json 文件用来配置 tsc 的编译配置选项。

当使用 tsc 并不指定 要编译的ts文件 的情况下,会从当前运行命令所在的目录开始逐级向上查找 tsconfig.json 文件。

我们也可以通过 --project(-p) 来指定一个包含 tsconfig.json 文件的目录来进行编译。

基本配置说明

  • compilerOptions:编译相关设置
    • module:指定编译后的代码要使用的模块化系统
    • target:指定编译后的代码对应的ECMAScript版本
    • outDir:指定编译后的代码文件输出目录
    • outFile:将输出文件合并成一个文件(合并的文件顺序为加载和依赖顺序)
  • include:指定要包含的编译文件目录(如src/**/*.ts
    • []:目录数组,使用 glob 模式
      • *匹配0或多个字符(不包括目录分隔符)
      • ?匹配一个任意字符(不包括目录分隔符)
      • **/递归匹配任意子目录
  • exclude:指定不要包含的编译文件目录
    • []:设置同 include ,默认会排除 node_modules 和 指定的目录

类型系统

类型注解(类型声明、类型约束)

JavaScript 是动态语言,变量随时可以被赋予不同类型的值,变量值的类型只有在运行时才能决定

  • 在编码(编译)阶段无法确定数据类型,会给程序在实际运行中带来极大的隐患
  • 不利于编码过程中的错误排查

使用类型注解能够在变量声明的时候确定变量存储的值的类型,用来约束变量或参数值的类型,这样在编码阶段就可以检查出可能出现的问题,避免把错误带到执行期间。

语法

let 变量: 类型

当变量接收了与定义的类型不符的数据会导致编译失败(警告)。

类型

typescript定义的类型包括:

  • 数字、字符串、布尔值
  • null、undefined
  • 数组、元组、枚举
  • void、any、Never

数字、字符串、布尔值

string、number、boolean:基本类型 String、Number、Boolean:对象类型

注意:基本类型可以赋值给对应包装对象,包装对象不可以赋值给对应基本类型

数组

数组声明语法

基本语法:

let list: number[];

泛型方式:

let list: Array<number>;

注意:

  • 具有相同类型的一组有序数据的集合
  • 声明数组同时要确定数组存储的数据的类型
  • 同一个数组中的数据只能有一种类型

元组

  • 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
  • 对于下标内的数据,数据顺序必须与声明中的类型一一对应
  • 对于越界下标数据,使用联合类型(声明类型的集合)
let data1: [number, string, boolean];

// 注意:顺序要对应
data1[0] = 1;
data1[1] = '1';
data1[2] = true;

// 对于超出(越界)部分,采用的是联合类型,或
data1[3] = false;   //存储number,string,boolean都可以,但是不能是其他的
// data1[4] = {};   //这样是不允许的

联合类型、交叉类型

联合是指多个类型中的一个,或的关系:

let v: string|number|boolean

交叉类型是指多个类型的叠加,并且的关系:

let v:string&number&boolean

枚举

使用枚举可以为一组数据赋予友好的名字。

我们有一种场景,比如:

let gender:number = 1; //1:男,2:女

if (gender == 1) {  

} else {

}

非常容易遗忘1表示的是什么。但如果这样来写:

enum Gender {Male, Female}; // enum Gender {Male=0, Female=1};

if (Gender.Male) {}

就会显得比较清晰。

默认情况下,元素编号从0开始,也可以手动编号。

类型推导

有的时候不一定需要强制使用类型声明,在某些情况下 TS 可以根据语境进行类型推导

  • 变量初始化:TS 会根据变量初始化的时候赋予的值进行类型推断
  • 上下文推断:TS 也会根据上下文进行类型的推断,比如在事件函数中,函数的第一个参数会根据当前绑定的事件类型推断处理事件对象

函数

语法

function fn(x: Type, y: Type): Type {}
let fn: (x: Type, y: Type) => Type = 函数实体
//  如果函数没有返回值,使用 void,不是undefined
function fn(x: number, y: number): number {
    return x + y;
}
// 函数表达式
let fn = function(x: number, y: number): number {
    return x + y;
}
let fn: (x: number, y: number) => number = function(x: number, y: number): number {
    return x + y;
}
// 根据类型推断可以简写
let fn: (x: number, y: number) => number = function(x, y) {
    return x + y;
}

可选参数

  • 通过 ? 来定义可选参数 function fn(x: Type, y?: Type): Type
  • 可选参数默认为 undefined
  • 可选参数必须在必传参数之后
function fn(x: number, y?: number): void {}

参数默认值

  • 参数默认值与 JavaScript(ES6)一致
  • 有默认值的参数不是必须在必填参数之后,但是不推荐如此
  • 有默认值的参数可以不需要明确类型约束
function fn(x: number, y = 1): void {
    console.log(y);
}

剩余参数

  • 参数默认值与 JavaScript(ES6)一致
  • 剩余参数类型为数组
  • 如果剩余参数类型多余一个,可以使用 Tuple
function fn(...args: any[]) {
    console.log(args);
}

函数重载

  • 用同名函数实现不同功能
  • 名称相同,但参数个数、类型、顺序不同
// 函数重载
function fn(x: number, y: number): number;
function fn(x: string, y: string): string;

function fn(x: any, y: any): any {
    return x + y;
}

this

因为普通函数中的 this 具有执行期绑定的特性,所以在 ts 中的this 在有的时候会指向隐式的指向类型 - any(并不是所有,比如事件函数)

在tsconfig中有一个配置项:noImplicitThis,来指出 this 隐式 any 类型的错误。

alt

this参数

我们可以在函数参数中提供一个显示的 this 参数,this 参数是一个假的参数,它出现在参数列表的最前面。

let obj1 = {
    a: 1,
    fn(this: Element|Document) {    // 在ts中函数的第一个this参数是用来设置this类型约束的
        // 这个this是一个假参数,运行过程中是不存在,是给ts检测使用的
        // console.log(this);
        // 希望ts是按照事件函数中的this去做检测
        this;   //检测检测检测
    }
};

与 ES2015 中的 class 类似,同时新增了很多实用特性。

成员属性与成员方法

与 ES2015 不同,TS 中的成员属性可以提取到构造函数以外进行定义

修饰符

通过修饰符可以对类中成员属性与成员方法进行访问控制: public、protected、private、readonly

class Person {
    /**
     * ts中的类,成员属性必须要声明后使用
     * ts中类的成员属性不是在构造函数中声明的,是在class内,方法外
     * 
     * public
     *      公开的,所有的地方都能访问,属性和方法默认是public
     * protected
     *      受保护的,在类的内部和他的子类中才能访问
     * private
     *      私有的,只能在该对象(类)的内部才可以访问
     */

    public username: string = '';
    // private username: string = '';
    // protected username: string = '';

    // readonly username: string = '';

    constructor(name: string) {
        this.username = name;
    }
}

存取器

TS 支持 getters/setters 来截取对对象成员的访问

class Person {

  username: string = 'zpu';

  private _age: number = 10;

  // 存取器,这个a并不会作为方法,而是属性去访问
  get age(): number {
      return this._age;
  }

  set age(age: number) {
      if (age > 0 && age < 150) {
          this._age = age;
      }
  }

}

let p1: Person = new Person();

/**
 * 允许在外部获取和修改age的值,但是不希望该修改成非法的,比如1000岁
*/
p1.age = 1100;

静态成员

类的一般成员属性和方法都属于实例对象的,也就是原型链上的,静态成员属于类(也就是构造函数)的,静态成员不需要实例化对象,直接通过类即可调用

/**
 * 通过某种方法控制系统同时只有一个Mysql的对象在工作
 * 通过口头去约定是不靠谱的
 */
class Mysql {

    // 静态属性,不需要通过new出来的对象方面,直接是通过Mysql类来访问
    public static instance;

    host: string;
    port: number;
    username: string;
    password: string;
    dbname: string;

    private constructor(host = '127.0.0.1', port = 3306, username='root', password='', dbname='') {
        this.host = host;
        this.port = port;
        this.username = username;
        this.password = password;
        this.dbname = dbname;
    }

    public static getInstance() {
        if (!Mysql.instance) {
            Mysql.instance = new Mysql();
        }
        return Mysql.instance;
    }

    query() {}
    insert() {}
    update() {}

}

继承

TS 中类可以通过 extends 类进行继承。

  • extends 关键字
  • 单继承
  • super 关键字
  • 修饰符(private的不可以继承)
class Person {

  private _a = 1;

  // 在构造函数的参数中如果直接使用public等修饰符,则等同于同时创建了该属性
  constructor(public username: string, public age: number) {
      this.username = username;
      this.age = age;
  }

}

class Student extends Person {
  // 如果子类没有重写构造函数,则直接父类的
  // 如果子类重写了构造函数
  // 注意:需要手动调用父类构造函数
  // super:关键字,表示父类
  constructor(username: string, age: number, public type: string) {
      super(username, age);    //执行父类构造函数
      this.type = type;
  }
}

let s1 = new Student('zpu', 31, 'javascript');

抽象类

类是对具有相同特性的对象的抽象,抽象是对具有相同特性的类的抽象,当派生类(子类)具有的相同的方法但有不同实现的时候,可以定义抽象类并定义抽象方法。

  • 抽象方法只定义结构不定义实现
  • 拥有抽象方法的类必须是抽象类,但是抽象类不一定拥有抽象方法,抽象类中也可以包含有具体细节的方法
  • abstract 关键字可以与 修饰符一起使用
  • 继承了抽象类的子类必须实现了所有抽象方法才能被实例化,否则该子类也必须声明为抽象的
abstract class Person { //抽象类不能实例化的
    username: string;

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

    say() {
        console.log('哈哈哈哈哈');
    }

    // 虽然子类都会有这样的特性,学习,但是子类学习具体过程不一样,所在在父类确定
    // 不了study方法的具体实现,父类只能有抽象约定,接收什么参数,返回什么内容
    // 如果一个类中有抽象的方法了,那么这个类也必须是抽象的
    abstract study(): void   //抽象方面是没有具体代码的
}


class Student extends Person {

    study() {
        console.log('学生有学生的学习方法 - 需要老师教授');
    }

}

class Teacher extends Person {

    study() {
        console.log('自学');
    }

}

// 如果一个类继承了抽象的父类,就必须实现所有抽象方面,否则这个子类还是必须得为抽象的
// abstract class P extends Person {

// }

接口

基础

  • 类型检查器会检查变量是否符合接口定义的结构
  • 类型检查器只会检查必须的属性是否存在,以及类型是否匹配
  • 类型检查器不会检查属性的顺序
/**
 * interface
 *      为我们提供一种方式来定义某种结构,ts按照这种结构来检测数据
 * 
 *      写法
 *          interface 接口名称 {
 *              // ... 接口规则
 *          }
 * 
 *  接口中定义的规则只有抽象描述,不能有具体的值与实现的
 * 
 *  对象抽象 => 类(对象的抽象描述)
 *  类抽象 => 抽象类(如果一个类中拥有一个没有具体实现的抽象方法,就是抽象类)
 *  抽象类 => 接口(如果一个抽象类的成员全部是抽象的,那么可以看做接口)
 */

interface Options {
    width: number,
    height: number
}

function fn(opts: Options) {}

// 类型检测只检测必须的属性是否存在,不会按照顺序进行,无序的
fn({
    height: 200,
    width: 100
});

可选结构

/**
 * 如果规则中有些是可选的,那么通过 ? 标识
 */

interface Options {
    width: number,
    height: number,
    color?: string
}

function fn(opts: Options) {}

fn({
    height: 200,
    width: 100
});

绕开类型检测

类型断言
    fn({} as Interface)
通过变量进行转换,先存变量,如何把变量作为参数传入
    let obj = {...}
    fn(obj)
索引签名
    interface 接口名{
        [attribute: string]: any
    }
interface Options {
    width: number,
    height: number,
    color: string
}

function fn(opts: Options) {}

// 告知ts检测,我传入的就是一个Options
fn({
    height: 200,
    width: 100
} as Options);

// 先赋值给一个变量,也可以绕开 检测
// let obj = {
//     height: 200,
//     width: 100,
//     color: 'red',
//     a: 1
// }
// fn( obj );
// 索引签名
/**
 * 希望规则是:一组由数字进行key命名的对象
 * 我们可以使用索引签名
 *  为数据定义一组具有某种特性的key的数据
 *  索引key的类型只能是 number 和 string 两种
 */

interface Options {
    // key 是number,value是any类型的数据
    [attr: number]: any,
    length: number
}

function fn(opt: Options) {}

fn({
    0: 100,
    1: 100,
    2: 2000,
    length: 1
});

函数类型接口

  • 通过接口的形式来定义函数
  • 函数类型接口并不是定义对象方法
interface 接口名{
    (param1: string, param2: string): boolean;
}
/**
 * 这个接口描述的是一个包含有fn并且值的类型为函数的结构体,并不是描述函数结构
 * 而是一个包含函数的对象结构
 */
interface Options {
    fn: Function
}

let o: Options = {
    fn: function() {}
}
interface IFn {
    (x: number, y: number): number
}


let fn: IFn = function(x: number, y: number): number {return x + y}
interface ResponseCallBack {
    (rs: Response): any
}

function todo(callback: ResponseCallBack) {
    callback(new Response);
}
interface AjaxData {
    code: number;
    data: any
}
interface AjaxCallback {
    (rs: AjaxData): any
}


function ajax(callback: AjaxCallback) {
    callback({
        code: 1,
        data: []
    });
}


ajax( function(x: AjaxData) {
    x.code
    x.data

    x.message // 这个就会报错
} );

类接口

  • 继承接口的类必须拥有接口定义的必须属性或方法
  • 一个类可以实现多个接口
  • 接口之间也可以继承
/**
 * 类接口
 *      使用接口让某个类去符合某种契约
 * 
 * 类可以通过 implements 关键字去实现某个接口
 *      - implements 某个接口的类必须实现这个接口中确定所有的内容
 *      - 一个类只能有一个父类,但是可以implements多个接口,多个接口使用逗号分隔
 */

interface ISuper {
    fly(): void;
}

class Man {

    constructor(public name: string) {
    }

}

class SuperMan extends Man implements ISuper {

    fly() {
        console.log('起飞');
    }

}

class Cat {

}

class SuperCat extends Cat implements ISuper {
    fly() {
        console.log('起飞');
    }
}

let p = new SuperMan('zpu');

案例

一个http的例子,拿ajax的options来说,有时候可能手误写成了methods,那用js就检查不出来了,但ts就不一样了:

interface HttpOptions {
    method: string,
    url: string,
    isAsync: true
}
interface HttpResponseData {
    code: number,
    data: any
}

function http(options: HttpOptions) {

    let opt:HttpOptions = Object.assign({
        method: 'get',
        url: '',
        isAsync: true
    }, options);

    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        xhr.open(opt.method, opt.url, opt.isAsync);

        xhr.onload = function() {
            let data: HttpResponseData = JSON.parse(xhr.responseText);
            resolve( data );
        }

        xhr.onerror = function() {
            reject({
                code: xhr.response.code,
                message: '出错了'
            });
        }

        xhr.send();
    })
}

http({
    method: 'get',
    url: '....',
    isAsync: true
}).then( (rs: HttpResponseData) => {
    rs.code
} );

泛型

通常我们会使用变量来表示是一个可变的值,通过变量我们就可以使代码具有很高的可重用性,但是在有类型约束的语言中,有时候不利于代码的复用,通过使用泛型,我们就可以解决这个问题,简单的理解可以说是给类型定义变量。

泛型变量
    function fn(arg: string): string{}
    function fn<T>(arg: T): T{}
    function fn<T, S>(arg1: T, arg2: S): [T,S]{}
数组形式
    function fn<T>(arg: T[]): T[]{}
    function fn<T>(arg: Array<T>): Array<T>{}
泛型类型
    把泛型作为一种类型使用
    let fn: <T>(arg: T) => T;
泛型接口
    interface 接口名<T> {
        <T>(arg: T): T;
    }
    let fn: 接口名<number> = function T(arg: T): T {
        return arg;
    }
泛型类
    class 类名<T> {}

泛型约束
    <T extends 类型>

类类型
    <T>(c: {new(): T})

基础

/**
 * 泛型
 *  很多时候,类型是写死的,这样就不利于复用
 * 
 *  这样,我们就需要给类型这种值也可以设置变量
 *  <类型变量名>,一般系统使用单字母大写,比如 T,E...
 *  写在函数名,参数列表之间,这是函数的
 */
function fn<T>(x: T): number {
    return Number(x) * 10;
}

fn(1);  //在调用fn函数的时候,同时给T赋值number
fn<number>(1);
function fn<T, S>(arg1: T, arg2: S): [T,S]{
    return [arg1, arg2];
}

let a = fn<string, number>('a', 1);

// function fn<T>(arg: T[]): T[]{
//     return arg;
// }

// fn<string>( ['1','2'] );
// fn<string>( [1,2] );

泛型类

class MyArray <T> {

    private _data: T[] = [];

    constructor() {

    }

    public mypush(v: T): number {
        this._data.push(v);
        return this._data.length;
    }

}

// 对于arr对象这个实例来讲,里面的T就是string
let arr: MyArray<string> = new MyArray();
arr.mypush('1');

// 对于arr对象这个实例来讲,里面的T就是number
let arr2: MyArray<number> = new MyArray();
arr2.mypush(1);

泛型接口

interface IFn <T> {
    (x: T, y: T): number
}

let fn: IFn<string> = function(x, y) {
    return Number(x) + Number(y);
}

类类型

function getArray(constructor: {new(): Array<string>}) {   // {new()} 接收一个可以产生对象的构造函数
    return new constructor();
}

getArray( Array  );

泛型约束

interface Len {
    length: number
}

function fn<T extends Len>(a: T) {
    // 不是所有类型都有length
    a.length
}

fn('1');

装饰器

在尽可能不改变类(对象)结构的情况下,扩展其功能。

  • 启用装饰器模式:--experimentalDecorators
  • 装饰器是一种特殊类型的声明,它可以被附加到类声明、属性、方法、参数或访问符上

alt

装饰器函数

我们要在一个类或方法上使用装饰器,首先需要提供一个装饰器函数,这个函数会在该装饰器被使用的时候调用。

使用装饰器,在需要被装饰的类或方法前通过 @装饰器名称 来调用装饰器。

@f
class 类名 {}

装饰器可以累加,可以一行也可以多行书写。

function Age<T extends {new(...args: any[]): {}}>(constructor: T): T {
    // console.log(1)
    class Person2 extends constructor {
        age: number = 0;
    }
    return Person2;
}

// Age是一个装饰器函数,该函数会自动调用,不需要加()调用,调用的时候会传入下面这个对应
// 的class的构造函数
@Age
class Person {
    username = 'zpu'
}

let p1 = new Person();
console.log(p1);
// 希望装饰出来的age属性的值不是固定的
// 装饰器函数不是我们主动调用的
// 如果我们希望传入构造值,那么就得使用 : 闭包
// Age就不能直接作为装饰器函数
// 该函数执行完成以后需要返回一个函数,这个返回的函数将作为实际的装饰器函数
function Age(v: number) {
    // 这个返回的函数才是真正的装饰器要执行的函数
    return function<T extends {new(...args: any[]): {}}>(constructor: T): T {
        class Person2 extends constructor {
            age: number = v;
        }
        return Person2;
    }
}

@Age(10)
class Person {
     username = 'zpu'
}

@Age(20)
class Cat {
    username = '小猫咪'
}

let p1 = new Person();
console.log(p1);

let c1 = new Cat();
console.log(c1);

方法修饰器

用来监视、修改或者替换方法定义。

方法装饰器会在调用时传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象
  • 成员的名称
  • 成员属性描述符

ts写node项目

ts-node

TypeScript execution environment and REPL for node.

使用ts-node和vsc来调试TypeScript代码

另外,还有一个babel-node,其实就是可以用来实现在node中写es6的代码,只要配好babel就行。

另外我们还需要结合nodemon,不然每次改下代码就得重启服务,比较蛋疼:How to watch and reload ts-node when TypeScript files change

声明文件

在我们使用koa的时候,它会提示下面的错误:

alt

需要我们安装一下@types/koa

npm install @types/koa -D

一些疑问

N久之前,我看typescript使用jquery模块,得这样来写:

import * as $ from 'jquery';

其实这是因为jquery的声明文件导出是这样的:

alt

当然我们也可以写成类似上面的:

import $ = require('jquery');

reflect-metadata

reflect意为反射。JS的反射学习和应用

TypeScript支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json里启用emitDecoratorMetadata编译器选项。

这里有一篇文章,大家可以学习一下:TypeScript 中的 Decorator & 元数据反射:从小白到专家(部分 IV)

koa-controllers就是基于这个的。

alt

当然在我看来,有这个还是不够的,还需要像@validate,用来校验body的参数啥的。不过上面的库,我也没有深入去看,是否能支持啥的,只是突然想到了而已。

基本使用

// app入口
import "reflect-metadata";
import Koa = require('koa');
import { useControllers } from 'koa-controllers';

const app = new Koa();
useControllers(app, __dirname + '/controllers/*.ts', {
    multipart: {
        dest: './uploads'
    }
});

app.listen(8080);
import * as Koa from 'koa';
import {Controller, Get, Ctx,} from 'koa-controllers'

console.log(1)

@Controller
export default class MainController {

    @Get('/')
    public async index( @Ctx ctx: Koa.BaseContext ) {
        ctx.body = 'Hello';
    }

    @Get('/user')
    public async user( @Ctx ctx: Koa.BaseContext ) {
        ctx.body = '用户中心'
    }

}

运行npx ts-node src/app.ts,就可以把服务跑起来了。

书写自己的声明文件

why?

比如说我们在ts中写这样的代码:

import configs = require('../config/config.json');

Cannot find module '../config/config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension

做法

ts默认不能把json文件作为模块去处理(加载),所以我们可以通过写一个声明文件。(.tsconfig中有一个typeRoots的配置,可以指定要到哪个文件夹去找声明文件,默认它会去从node_modules/@types,我们可以多加一个自己的声明文件夹)。

// json.d.ts
declare module "*.json" {
    const value: any;
    export default value;
}

TypeScript 中的声明文件

vue中使用ts

vue-property-decorator使用 TypeScript/装饰器增强 Vue 组件。

vue + typescript 项目起手式

结语

TS基础看下来,感觉类类型的写法确实些蒙。。

不过咋说呢,ts是一种趋势,起码在库、框架层面可以考虑使用起来。等明年有空了,准备照着antd抄一下,一边抄一边理解,也许会对ts的理解更深入一些。

本文链接:www.my-fe.pub/post/ts-getting-started.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。