TypeScript 学习(上篇)

209 阅读11分钟

前言:

TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。

学习 TypeScript 的优势

TypeScript 增加了代码的可读性和可维护性,在代码进行重构和维护时,提供了相当舒适的编写体验。

基础类型

  • 布尔:boolean
  • 数字:number
  • 字符串:string
  • 数组:[]
  • 由数字组成的数组: number[]
  • 元组( 表示一个已知元素数量和类型的数组):如
    • let x: [string, number] //只接受一个第一项为 string,第二项为 number 的 一个数组
  • 枚举:enum(默认情况下,从 0 开始为元素编号,用户可以手动定义赋值),例:
enum Color {Red, Green, Blue}
let c: Color = Color.Green

enum Color {Red = 1, Green = 2, Blue = 4}
  • 任意类型: any。变量如果在声明的时候,未指定类型,那么默认指定类型就是 any
  • 无返回值(一般用于 function 函数无返回值):void
  • null undefined:null 和 undefined 是所有类型的子类型
  • 永不存在的值:never
  • 对象:object
  • 联合类型:
let demo: string | number;
demo = 'seven';  //true
demo = 7;  //true
demo = true;  //error

类型断言

有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。

  • 方法一:“尖括号语法”
let someValue: any = "this is a string"; 
let strLength: number = (<string>someValue).length;
  • 方法二:as 语法
let someValue: any = "this is a string"; 
let strLength: number = (someValue as string).length;

接口

在 TypeScript 中,我们使用接口(Interface)来定义对象的类型。

什么是接口?

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

  • 接口一般首字母大写 例:
interface LabelValue { 
    label: string; 
} 
function printLabel(labelObj: LabelValue) {    
    console.log(labelledObj.label); 
} 

let myObj = {size: 10, label: "Size 10 Object"}; 
printLabel(myObj);
  • 接口就好比一个名字,用来描述上面例子里的要求。需要注意的是,我们在这里并不能像在其它语言里一样,我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
  • 还有一点是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。

  • 优点
    • 一是可以对可能存在的属性进行预定义
    • 二是可以捕获引用了不存在的属性时的错误
interface SquareConfig { 
    color?: string; 
    width?: number; 
}
function createSquare(config: SquareConfig){
    ...
}

let mySquare = createSquare({color: "black"});

任意属性

有时候我们希望接口允许有任意的属性,可以使用 [propName: string]: any;

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

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};
  • 但是,要注意的是,一旦用了任意属性,那么这个对象的所有属性都得符合这个任意类型的定义,例:
interface Person {
    name: string;
    age?: number;  
    [propName: string]: string;
}

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

//error Property 'age' of type 'number' is not assignable to string index type 'string'.
  • 同时,一个接口中只能定义一个任意属性,如果接口内有多个类型的属性,则可以使用联合类型
interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

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

只读属性 readonly

只能在对象刚刚创建的时候修改其值,相当于是 const

interface Point { 
    readonly x: number; 
    readonly y: number; 
}

通过赋值一个对象字面量来构造一个Point。 赋值后, x和y再也不能被改变了。

let p1: Point = { x: 10, y: 20 }; 
p1.x = 5; // error!
  • 使用 readonly 还是 const 呢? 最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用 readonly。

  • TypeScript 具有 ReadonlyArray 类型, 与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改

let a: number[] = [1, 2, 3, 4]; 
let ro: ReadonlyArray<number> = a; 
ro[0] = 12; // error! 
ro.push(5); // error! 
ro.length = 100; // error! 
a = ro; // error!
a = ro as number[]; // true!

类数组

类似数组的东西,但不是数组,不是数组类型,比如 arguments

function sum() {
    let args: number[] = arguments;
}
//error, 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;
}

继承接口:

从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape { 
    color: string; 
} 
interface Square extends Shape { 
    sideNum: number; 
} 
let square = <Square>{}; 
square.color = "blue"; 
square.sideNum= 10;

public

公有类,TypeScript里,成员都默认为 public

private

私有类,当成员被标记成 private时,它就不能在声明它的类的外部访问

class Animal { 
    private name: string; 
    constructor(theName: string) { 
        this.name = theName; 
    } 
} 
new Animal("Cat").name; // error: 'name' 是私有的.

protected

与 private 类似,但是 projected 再派生类中可以访问,private 则不行

抽象类

做为其它派生类的基类使用, 一般不会直接被实例化

abstract class Animal { 
    abstract makeSound(): void; 
    move(): void { 
        console.log('roaming the earch...'); 
    } 
}

let animal: Animal; // true,允许创建一个对抽象类型的引用
let animal= new Animal(); // error: 不能创建一个抽象类的实例

函数

函数类型

输入多余的(或者少于要求的)参数,是不被允许的

function sum(x: number, y: number): number {
    return x + y;
}

sum(1, 2, 3);  //error
  • 注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。
  • 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
let mySum: (x: number, y: number) => 
number = function (x: number, y: number): number {
    return x + y;
};
  • 除了描述带有属性的普通对象外,接口也可以描述函数类型。 就像是一个只有参数列表和返回值类型的函数定义。
interface S { 
    (a: string, b: string): boolean; 
}

let mySearch: S; 
mySearch = function(a: string, b: string) { 
    let result = source.search(subString); 
    return result > -1; 
}

可选参数

传递给一个函数的参数个数必须与函数期望的参数个数一致。

function demoName(firstName: string, lastName: string) { 
    return firstName + " " + lastName; 
}
let result1 = buildName("Bob"); // error, too few parameters 
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters 
let result3 = buildName("Bob", "Adams"); // true

在 TypeScript 里我们可以在参数名旁使用 ?实现可选参数的功能。 比如,我们想让 lastName 是可选的:

function buildName(firstName: string, lastName?: string) { 
    if (lastName){ 
        return firstName + " " + lastName; 
    } else{ 
        return firstName; 
    }
}

let result1 = buildName("Bob"); // true
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters 
let result3 = buildName("Bob", "Adams"); // true
  • 注意: 可选参数必须跟在必须参数后面。也就是说,可选参数后面不允许再出现必需参数了:

参数默认值

TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tom1 = buildName('Tom', 'Cat'); //true
let tom2 = buildName('Tom'); //true
  • 这时 lastName 就会被 typescript 设置为可选参数,并且不受“可选参数必须接在必需参数后面” 的限制

剩余参数

function buildName(firstName: string, ...restOfName: string[]) { 
    return firstName + " " + restOfName.join(" "); 
} 
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
  • 实际上,restOfName 默认就是一个数组,所以可以定义为 string[]
  • 注意,rest 参数只能作为最后一个参数使用

重载

一个函数在接收不同数量或者不同类型的参数时,作出对应的不同的处理

function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}
  • 但这个方法有一个缺点: 不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。
  • 这时我们可以使用重载定义多个 reverse 的函数类型
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}
  • 前两次是函数的定义,第三次是函数的实现
  • 注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

类型断言

语法

as 类型 
// 或者
<类型>值
  • 注意:在 tsx 语法中,必须使用 值 as 类型 的形式
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof animal.swim === 'function') {
        return true;
    }
    return false;
}
//error Property 'swim' does not exist on type 'Cat | Fish'
  • 这时候就需要使用到类型断言,animal as Fish
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}
  • 但是要注意的是,类型断言只能够“欺骗”typescript 的编译器,无法去避免运行时的错误
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function swim(animal: Cat | Fish) {
    (animal as Fish).swim();
}

const tom: Cat = {
    name: 'Tom',
    run() { console.log('run') }
};
swim(tom);
  • 上面的例子编译时不会报错,但在运行时会报错。

  • 原因是 (animal as Fish).swim() 这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。

  • 可是 swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误了。

有的时候,我们非常确定这段代码不会出错

window.foo = 1; // error

  • 此时可以使用 as any 将 window 断言为 any 类型

(window as any).foo = 1

  • 但是,把一个变量断言为 any 只能作为最后的手段,尽可能不使用

声明文件

什么是声明文件?

通常,我们会把声明语句放到一个单独的文件(xxx.d.ts)中,这就是声明文件

  • 声明文件必需以 .d.ts 为后缀
// src/xxx.d.ts
declare var xxx: (selector: string) => any

// src/index.ts
xxx('#demo')
  • ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 xxx.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得这个 xxx 的类型定义了

声明语法

  • declare var 声明全局变量

    • 除了 var 以外,还有 declare letdeclare const 与普通的 let 和 const 没有区别。一般来说,全局变量都是不允许修改的变量,所以大部分情况下都应该使用 declare const
  • declare function 声明全局方法

  • declare class 声明全局类

  • declare enum 声明全局枚举类型

  • declare namespace 声明(含有子属性的)全局对象

    • 用来表示全局变量是一个对象,包含很多子属性。
    declare namespace jQuery {
    	function ajax(url: string, settings?: any): void;
    }
    
    jQuery.ajax('/xxx')
    
    • 注意,在declare namespace 内部,我们直接使用 function ajax 来声明函数,而不是使用 declare function ajax。类似的,也可以使用 const, class, enum 等语句
    declare namespace jQuery {
    	function ajax(url: string, settings?: any): void;
    	const version: number;
    	class Event {
        	blur(eventType: EventType): void
    	}
    	enum EventType {
        	CustomClick
    	}
    }
    
    jQuery.ajax('/api/get_something');
    console.log(jQuery.version);
    const e = new jQuery.Event();
    e.blur(jQuery.EventType.CustomClick);
    
    • 如果对象拥有深层的层级,则需要用嵌套的 namespace 来声明深层的属性的类型
    declare namespace jQuery {
    	function ajax(url: string, settings?: any): void;
    	namespace fn {
        	const demo: string 
    	}
    }
    
    jQuery.ajax('/api/get_something');
    console.log(jQuery.fn.demo)
    
    • 为了防止命名冲突, 暴露在最外层的 interfacetype 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 namespace
  • interfacetype 声明全局类型

  • 注意,声明语句只能定义类型,不能在里面定义具体实现

declare const jQuery = function(selector) {
    return document.querySelector(selector);
};
// ERROR: An implementation cannot be declared in ambient contexts.
  • .d.ts 文件仅仅会用于编译时的检查,声明文件里的内容在编译结果中会被删除。

声明合并

假如 jQuery 既是一个函数,可以直接被调用 jQuery('#foo'),又是一个对象,拥有子属性 jQuery.ajax(),那么我们可以组合多个声明语句,它们会不冲突的合并起来。

declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}

export 和 declare

export const name: string;
export function getName(): string;
export class Animal {
    constructor(name: string);
    sayHi(): string;
}
export enum Directions {
    Up,
    Down,
    Left,
    Right
}
export interface Options {
    data: any;
}

import { name, getName, Animal, Directions, Options } from 'foo';
  • 可以使用 declare 先声明多个变量,最后再用 export 一次性导出,所以上面的可以改成
declare const name: string;
declare function getName(): string;
declare class Animal {
    constructor(name: string);
    sayHi(): string;
}
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
interface Options {
    data: any;
}

export { name, getName, Animal, Directions, Options };

import { name, getName, Animal, Directions, Options } from 'foo';
  • export default 使用默认导出,此时使用 import 导入就使用的是 import xxx from 'xxx' 的形式
  • 要注意的是,只有 functionclassinterface 可以直接默认导出,其他变量需要先使用 declare再使用 export default 导出

模块插件

有时需要改变一个原有模块的结构,需要通过 import 导入一个插件,这是用就需要使用 ts 的 declare module,用来扩展原有模块的类型

  • 首先需要先引用原有模块,再使用 declare module 扩展
// types/moment-plugin/index.d.ts
import * as moment from 'moment';

declare module 'momentPlugin' {
    export function foo(): moment.CalendarKey;
}

// src/index.ts
import * as moment from 'moment';
import 'moment-plugin';

momentPlugin.foo();

内置对象

内置对象指的是在全局作用域(Global)上存在的对象

  • 例如 JS 的 BooleanErrorDateRegExp
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;
  • DOM 和 BOM 的内置对象, DocumentHTMLElementEventNodeList
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});