简介

Typescript 可以在代码编写写做类型检查,可以编写更健壮的代码。

安装

npm config set registry https://registry.npm.taobao.org
sudo npm install -g typescript
# 安装REPL
sudo npm install -g tsun

基本概念

联合类型

表示取值是多种类型中的一种,当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

let myFavoriteNumber: string | number;
myFavoriteNumber = "seven";
myFavoriteNumber = 7;

接口

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。 接口是一个类型,不是一个真正的值,它在编译结果中会被删除

//?可选属性
interface Person {
  name: string;
  age?: number;
}

let tom: Person = {
  name: "Tom",
};

let tom: Person1 = {
  name: "Tom",
  age: 25,
};

//接口可以继承
interface ApiError extends Error {
  code: number;
}

接口可以定义任意类型,但是当同时存在可选类型和任意类型,可选类型需要是任意类型的子集

interface Person {
  name: string;
  age?: number;
  //任意类型为联合类型
  [propName: string]: string | number;
}

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

接口属性只读,意味着,只有在创建对象时可被赋值,其后无法修改,而且只读属性必须在对象初始化时进行赋值。

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

let tom: Person = {
  id: 89757,
  name: "Tom",
  gender: "male",
};

数组

习惯性的将数组中的元素类型保持相同

let fibonacci: number[] = [1, 1, 2, 3, 5];
//泛型
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
//接口表示数组
//用接口表示数组通常用来标识类型
interface NumberArray {
  //索引是数字,类型是数字
  [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

interface IArguments {
  [index: number]: any;
  length: number;
  callee: Function;
}

函数

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression); 函数声明和函数表达式的词法环境和执行上下文是不一样的,函数声明会做类型提升。

// 函数声明(Function Declaration)
function sum(x, y) {
  return x + y;
}

// 函数表达式(Function Expression)
let mySum = function (x, y) {
  return x + y;
};

可以手动给函数表达式添加类型,也可以使用类型推断 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。 在 ES6 中,=> 叫做箭头函数,应用十分广泛

let mySum: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

可选参数用?标识,必须接在必需参数的后面

function buildName(firstName: string, lastName?: string) {
  if (lastName) {
    return firstName + " " + lastName;
  } else {
    return firstName;
  }
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");

ES6 中允许给参数添加默认值,Typescript 将添加默认值的参数识别为可选参数,并且添加默认值后,就不受「可选参数必须接在必需参数后面」的限制了 默认值参数在必需参数前的话,需要传一个 undefined 进去占位,因此推荐将默认值参数放在后面

function buildName(firstName: string = "Tom", lastName: string) {
  return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
//如果默认值参数在必需参数前边,必须传入undefined
console.log(buildName(undefined, "cat"));

function buildName1(firstName: string, lastName: string = "Man") {
  return firstName + " " + lastName;
}
console.log(buildName("good"));

剩余 rest 参数(不定长参数) rest 参数只能是最后一个参数

//items是一个数组
function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a: any[] = [];
push(a, 1, 2, 3);

函数重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。 Typescript 会从最前面的函数定义开始匹配

//前声明(定义)后实现,将精确的声明写在前面
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("");
  }
}

类型断言

值 as 类型 类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除 它不会真的影响到变量的类型。 应用场景:

  • 将一个联合类型断言为其中一个类型,欺骗 tsc 编译器,可能导致运行时出错
  • 将一个父类断言为更加具体的子类
  • 将任何一个类型断言为 any
  • 将 any 断言为一个具体的类型

限制: typescript 是结构类型系统,不关心定义时的类型关系,只比较最终结构有什么关系

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型
  • 要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可
//类型比较
//下面两种Cat的定义是等价的
interface Animal {
  name: string;
}
interface Cat {
  name: string;
  run(): void;
}

interface Cat extends Animal {
  run(): void;
}
//联合类型断言
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;
}
//子类断言
interface ApiError extends Error {
  code: number;
}
interface HttpError extends Error {
  statusCode: number;
}

function isApiError(error: Error) {
  if (typeof (error as ApiError).code === "number") {
    return true;
  }
  return false;
}
//确保代码无误后,绕过类型检查
//在类型的严格性和开发的便利性之间掌握平衡
(window as any).foo = 1;
//明确类型,后续有了代码补全,提高可维护性
function getCacheData(key: string): any {
  return (window as any).cache[key];
}

interface Cat {
  name: string;
  run(): void;
}

const tom = getCacheData("tom") as Cat;
tom.run();

类型声明比类型断言约束更严格,如 animal 断言为 Cat,只需要满足 Animal 兼容 Cat 或 Cat 兼容 Animal 即可 animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行 可以用泛型替代类型断言

function getCacheData<T>(key: string): T {
  return (window as any).cache[key];
}

interface Cat {
  name: string;
  run(): void;
}

const tom = getCacheData<Cat>("tom");
tom.run();

声明文件

常用的声明语法

  • declare var 声明全局变量
  • declare const 声明全局常量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interface 和 type 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// <reference /> 三斜线指令

类型别名

类型 c 语言 typedef,在 typescript 中用 type 创建类型别名

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}

字面量类型

约束取值只能是某几个值中的一个

type EventNames = "click" | "scroll" | "mousemove";
function handleEvent(ele: Element, event: EventNames) {
  // do something
}

handleEvent(document.getElementById("hello"), "scroll"); // 没问题
handleEvent(document.getElementById("world"), "dblclick"); // 报错,event 不能为 'dblclick'

元组

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象; 可以对元组中的单个元素赋值; 当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项; 当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型

let tom: [string, number] = ["Tom", 25];
let tom: [string, number];
tom[0] = "Tom";
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);

tom = ["Tom", 25];
tom.push("male");

枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景

enum Days {
  Sun,
  Mon,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat,
}
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class 使用 class 定义类,使用 constructor 定义构造函数。 通过 new 生成新实例的时候,会自动调用构造函数。 使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。 使用 getter 和 setter 可以改变属性的赋值和读取行为: 使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用,不可以通过实例来调用:

ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义 ES7 提案中,可以使用 static 定义一个静态属性,静态属性属于类; 当构造函数修饰为 private 时,该类不允许被继承或者实例化; 当构造函数修饰为 protected 时,该类只允许被继承,不允许被实例化; 类属性/方法的访问限定符如下:

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
class Animal {
  private _name: string;
  age = 23;
  static num = 42;
  constructor(name) {
    this._name = name;
  }

  get name() {
    return "get " + this._name;
  }

  //name是public的,但是_name是私有的
  //不能在set name中再对name赋值,会造成死循环
  set name(value) {
    if (value === "Dog") {
      console.log("Animal cannot be dog");
      return;
    }
    this._name = value;
    console.log("setter: " + value);
  }

  sayHi() {
    console.log(`My name is ${this._name}`);
  }

  static isAnimal(a) {
    return a instanceof Animal;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name); // 调用父类的 constructor(name)
    console.log(this.name);
  }
  //函数重写
  sayHi() {
    return "Meow, " + super.sayHi(); // 调用父类的 sayHi()
  }
}

let a = new Animal("Jack");
Animal.isAnimal(a); // true

let c = new Cat("Tom"); // Tom
console.log(c.sayHi()); // Meow, My name is Tom

参数属性 修饰符和 readonly 还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值 只读属性关键字,只允许出现在属性声明或索引签名或构造函数中 注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。 abstract 用于定义抽象类和其中的抽象方法。 抽象类不允许被实例化,抽象类中的抽象方法必须被子类实现

abstract class Animal {
  //public readonly name;
  public constructor(public readonly name: string) {
    this.name = name;
  }
  //abstract method
  public abstract eat();
}

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

let a = new Cat("Tom");
console.log(a.name); // Tom

类和接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特 性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

  • 接口中所有属性和方法都要求是 public
  • 一个类可以实现一个或者多个接口
  • 接口之间可以是继承关系
  • 接口可以继承类(不推荐),本质上还是接口继承接口,因为在创建类的时候,会创建一个同名的接口类型

创建类时自动生成的类型中包含了除了构造函数的实例属性和实例方法,会保留访问限定符, 如果类属性是 private,将导致该类型的对象无法被初始化,生成的接口类型中不包括:

  • 静态类型和静态方法
  • 构造函数
interface Alarm {
  alert(): void;
}

interface Light {
  lightOn(): void;
  lightOff(): void;
}

class Car implements Alarm, Light {
  alert() {
    console.log("Car alert");
  }
  lightOn() {
    console.log("Car light on");
  }
  lightOff() {
    console.log("Car light off");
  }
}

接口继承类(晦涩)

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

interface PointInstanceType {
  x: number;
  y: number;
}

// 等价于 interface Point3d extends PointInstanceType
interface Point3d extends Point {
  z: number;
}

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

泛型

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

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray<string>(3, "x"); // ['x', 'x', 'x']

//多个类型参数

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

swap([7, "seven"]); // ['seven', 7]

泛型约束,可以使用其他类型约束,也可以在类型参数之间进行约束

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = (<T>source)[id];
  }
  return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

参考

Typescript 入门教程

深入理解Typescript