It's our wits that make us men.

typescript要点记录

Posted on By He Yan

参考资料: https://www.tslang.cn/docs/handbook/ https://www.typescriptlang.org/docs/handbook/

基础类型

ts中支持的基础数据类型

  • boolean:布尔值
  • number:数字
  • string:字符串
  • [] / number[] / Array<number>:数组
  • [string, number]:元组(表示一个已知元素数量和类型的数组,各元素类型不必相同)
  • enum:枚举(为一组数值赋予友好的名字)
  • any:不进行类型检查的任意类型
  • void:没有任何类型(通常表示无返回值)
  • null
  • undefined
  • never:永远不存在值的类型(总会抛出异常/无返回值的函数/箭头函数返回值/函数存在无法到达的终点)
  • object

类型断言

  • 表示人为确定某值应该当作什么类型去处理
  • 有两种写法:

    let val = "a string";
    // 尖括号语法
    let valLen1: number = (<string>val).length;
    // as 语法
    let valLen2: number = (val as string).length;
    

接口

  • 接口作用:
    1. 为类型命名
    2. 为代码定义契约
interface LabelledValue {
  label: string;
}

function printLabel(labelObj: LabelledValue) {
  console.log(labelObj.label);
}

// it‘s ok
printLabel({
  label: "hello",
});

// error TS2345(对象字面量会经过额外属性检查,若存在目标类型不包含的属性时就会报错)
printLabel({
  label: "hello",
  name: "world"
});

let obj = {
  label: "hello",
  name: "world"
}
// 变量引用可绕过额外属性检查
printLabel(obj);

// 使用类型断言可以绕过额外属性检查
printLabel({ label: "hello", name: "world" } as LabelledValue);


// 使用字符串索引签名在接口中定义一个属性,表示该类型对象会带有任意数量的其他属性
interface Student {
  name: string;
  [propName: string]: any;
}

对象类型接口

  • 可选属性:接口中定义可选的属性字段(对可能存在对属性进行预定义,引用不存在属性时将抛出错误)

    interface Person {
      name: string;
      age?: number;
    }
    
  • 只读属性:接口中定义只能在对象刚刚创建时修改其值的属性

    interface Person {
      readonly name: string;
    }
    

函数类型接口

  • 接口也可描述函数类型(包括参数类型和返回值类型)
interface QueryPerson {
  (name: string, age: number): boolean;
}
let ifPersonExist: QueryPerson;
// 参数名可以不一样,参数和返回值类型可以不写 ts会依据接口进行类型推断
ifPersonExist = function(name: string, age: number): boolean {
  return true;
}

可索引类型接口

  • 可索引类型具有一个索引签名,它描述对象索引(key)的类型,还有相应的索引返回值(value)的类型
  • ts支持两种索引类型:number,string
  • 同时使用两种索引类型时,二者的value类型需保持相同(因为都是转为string类型索引的)
  • 可以在接口中设置readonly,防止给索引赋值
interface StringArr {
  // 表示key为number类型,value为string类型,并且不可修改
  readonly [index: number]: string;
}

let myArr: StringArr;
myArr = ["bob", "fred"];

let firstStr: string = myArr[0];

类类型接口

  • 明确的强制一个类去符合某种契约
  • 接口描述了类的公共部分,而不是公共和私有两部分。它不会帮你检查类是否具有某些私有成员。
interface Person {
  name: string;
  sayHi(name: string);
}
class Student implements Person {
  name: string;
  sayHi(name: string) {
    console.log('hello', name);
  }
  constructor(name: string) { }
}

混合类型接口

  • 一个对象同时具备多种类型时需要的契约
  • 场景:接入js第三方库
// 假设一个对象同时可作为函数和对象使用,并带有额外的属性
interface Person {
  // 本身可当作函数执行
  (play: string): void;
  // 可获取的额外属性
  name: string;
  // 可获取的额外方法属性
  sleep(time: number);
}

function child(): Person {
  let person = <Person>function (play: string) {
    console.log("play", play);
  }
  person.name = "Jim";
  person.sleep = (time: number) {
    console.log("sleep", time);
  }
  return person;
}

let xiaoMin = child();

xiaoMing("foot ball");
xiaoMing.name = "xiaoMing";
xiaoming.sleep(20);

接口继承接口

  • 接口继承让我们可以灵活的分割接口,达到代码复用的目的。
interface Person {
  name: string;
}

interface Action {
  walk: boolean;
}

interface Student extends Person, Action {
  number: string;
}

let xiaoMing = <Student>{};
xiaoMing.name = "xiaoMing";
xiaoMing.number = "2016222";
xiaoMing.walk = true;

接口继承类

  • 继承类的成员但不包括其实现(只声明,不提供具体实现)
  • 该接口类型只能被这个类或其子类所实现(implements)
class Control {
  private state: any;
}

// 接口继承类
interface SelectableControl extends Control {
  select(): void;
}

// 该接口类型只能被这个类或者子类实现
class Button extends Control implements SelectbleControl {
  select() { }
}

class TextBox extends Control {
  select() { }
}

// error! Image类不是Control类或其子类,缺少state属性
class Image implements SelectableControl {
  select() { }
}

class Location {
  
}

函数

函数类型

函数类型包含了参数类型和返回值类型

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

可选参数和默认参数

  • 可选参数在参数名旁用“?”表示可选
  • 可选参数必须跟在必须参数后面
  • 默认参数表示传入参数为undefined时使用默认值
  • 使用扩展运算符收集剩余的多个参数

重载

  • 如果想定义的函数参数和返回值可能出现多种情况,为函数提供多个函数类型定义来进行函数重载
  • 编译器会从函数定义列表第一个开始尝试定义,所以第一个一定要是最精确的定义
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: { suit: string; card: number; }[]):number;
function pickCard(x: number): { suit: string; card: number; };
function pickCard(x): any {
  if (typeof x === 'object') {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  else if (typeof x === 'number') {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

字面量类型

  • 字面量类型是指一个抽象类型中更具体固定的子类型(值),比如“string”类型中的“hello world”它是一个具体值也是一个固定类型,“hello tom”不等于“hello world”,也就意味着“hello tom”不是“hello world”字面量类型。
  • 目前ts中有三种可用的字面量类型集合:字符串字面量类型集合、数字字面量类型集合、布尔字面量类型集合
  • 字面量类型通过type关键字或直接拿一个集合放在类型位置上定义。
type Easing = "ease-in" | "ease-out" | "ease-in-out";
type Size = 8 | 16 | 32;
type Disabled = true;

interface MapConfig {
  lng: number;
  lat: number;
  tileSize: 8 | 16 | 32;
}

联合类型和交叉类型(Unions and Intersection Types)

联合类型

联合类型表示可以使用符合列举的几种类型之一的数据(或关系)

// 字符串或者数字
function padLeft(value: string, padding: string | number) {
  // ...
}

交叉类型表示可以使用同时符合列举的几种类型的数据(与关系)

interface ErrHandling {
  success: boolean;
  error?: { message: string };
}

interface ArtworksData {
  artworks: { title: string }[];
}

interface ArtistsData {
  artists: { name: string }[];
}

// 合并类型(需要既具有Data接口定义的属性,又需要有handling定义的属性)
type ArtworksResponse = ArtworksData & ErrHandling;
type ArtistsResponse = ArtistsData & ErrHandling;

const handleArtistsResponse = (response: ArtistsResponse) => {
  // ...
}



  • 基于类的面向对象编程方式
class Greeter {
  // 属性
  greeting: string;
  // 构造函数
  constructor(message: string) {
    this.greeting = message;
  }
  // 方法
  greet() {
    // this表示访问类的成员
    return "Hello" + this.greeting;
  }
}

// 构造实例
let greeter = new Greeter("world");

继承

  • 使用继承扩展现有的类

例1:

// 基类(超类)
class Animal {
  move(distanceInMeters: number = 0) {
    console.log("Animal moved ", distanceInMeters);
  }
}

// 继承animal类  派生类(子类)
class Dog extends Animal {
  bark() {
    console.log("woof!");
  }
}

const dog = new Dog();
dog.bark();
dog.move(10);

例2:

class Animal {
  name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log("move", distanceInMeters);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    // 以name入参,执行基类的构造方法(必须的)
    super(name);
  } 
  // 重写基类的move方法
  move(distanceInMeters: number = 0) {
    console.log("Slithering");
    // 执行基类的move方法(基类:当作函数时会执行构造函数;当作对象时可获取属性和方法)
    super.move(distanceInMeters);
  }
}

class Horse extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 45) {
    console.log("Galloping");
    super.move(distanceInMeters);
  }
}

类成员修饰符

  • 这些修饰符修饰的成员都是当类被实例化时才会被初始化的属性,反过来讲是依附于实例存在的属性(区别于静态属性)
修饰符 含义 兼容比较
public(默认) 可被自由访问 不同声明处,但类型相同则兼容
private 可以在声明它的类外部访问 相同声明处,且类型相同则兼容
protected 可以在声明它的类和派生类中访问 相同声明处,且类型相同则兼容
readonly 只读属性,表示必须在声明时或构造函数里被初始化 -

例1: 默认修饰符public

class Animal {
  public name: string;
  public constructor(theName: string) {
    this.name = theName;
  }
  public move(distanceInMeters: number) {
    console.log("move", distanceInMeters);
  }
}

例2: private修饰的属性不可在其类外访问

class Animal {
  // 类的私有属性相当于只局限于该类内部自己使用的属性
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}

// err!私有属性不可在声明它的类外使用
new Animal("cat").name;

例3: 不同声明,相同类型的private修饰类型不兼容

class Animal {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}

class Rhino extends Animal {
  constructor() {
    super("Rhino");
  }
}

class Employee {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

// 继承了基类的子类和基类兼容
animal = rhino;
// 结构相同但属性不是同一处声明的不兼容
animal = employee; // err! Animal 与Employee不兼容

例4: protected成员与private类似,但是多了仍可以在派生类中访问到的性质

class Person {
  // 基类中声明的保护类型
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Employee extends Person {
  private department: string;
  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }
  public getElevatorPitch() {
    // 派生类中仍可以访问到基类中的保护类型成员
    return `Hello, my name is ${this.name}. I work in ${this.department}.`;
  }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
// err! 不可在基类和派生类外访问到保护成员
console.log(howard.name);

例5: 构造函数被标记为protected,意味着该类不可被实例化,但可被继承(不可在类外访问执行,但可在派生类中访问执行)

class Person {
  protected name: string;
  protected constructor(theName: string) {
    this.name = theName;
  }
}

class Employee extends Person {
  private department: string;

  constructor(name: string, department: string) {
    // 可在派生类中调用基类中保护类型的构造函数
    super(name);
    this.department = department;
  }

  public getElevatorPitch() {
    return this.name + this.department;
  }
}

let howard = new Employee("Howard", "Sales");
// err!不可在类外执行保护类型的构造函数
let john = new Person("John");

例6: 只读属性只能在声明时或构造函数里被初始化

class Octopus {
  readonly name: string;
  // 声明时初始化
  readonly numberOfLegs: number = 8;
  constructor (theName: string) {
    // 构造函数里初始化
    this.name = theName;
  }
}

let tom = nem Octopus('Tom');
// err! 只读属性
tom.name = "Tim";

参数属性

给构造函数的参数增加成员修饰符实现直接对成员的声明&初始化

class Octopus {
  readonly numberOfLegs: number = 8;
  constructor(readonly name: string) {

  }
}

存取器

  • getters/setters:截取对对象成员的访问和赋值
  • 只有get无set的存取器被自动推断为readonly

给普通类加上存取器的使用,使得获取和赋值操作必须经过中间动作

// ---普通类
class Employee {
  salary: number;
}

let tom = new Employee();
// 普通类成员可以被随意访问修改
tom.salary = 2500;
console.log(tom.salary);

// ---加入存取器
let password = '123';
class Employee {
  private _salary: number;

  get salary(): string {
    return this._salary + '';
  }

  set salary(newSalary: number) {
    if (password && password === '123') {
      this._salary = newSalary;
    } else {
      console.log("err");
    }
  }
}
let employee = new Employee();
employee.salary = 2500;
alert(employee.salary);

静态属性

静态属性只存在于类本身身上,而不是类的实例上,访问时直接以访问类对象的属性的形式访问

class Person {
  static eyes = 2;
  static legs = 2;
  getPersonFeature() {
    return Person.eyes + Person.legs;
  }
  constructor(public name: string) { }
}

let tom = new Person('Tom');
console.log(tom.getPersonFeature());

抽象类

  • 抽象类作为其他派生类的基类使用,一般不会被直接实例化(类的基础模板类)
  • 不同于接口,抽象类内部可以包含非抽象成员的实现细节
  • 通过abstract定义抽象类和抽象方法
  • 抽象方法不包含具体实现,必须在派生类中实现

抽象类举例

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

泛型(Generics)

  • 为了支持组件重用:一个组件支持多种类型的数据

泛型变量

  • 泛型变量用来指代数据的类型。它是一种特殊的变量,指类型而不是指值;用 <> 表示,在做定义时候使用

定义一个泛型函数

// <T> 声明了一个泛型变量T,然后在后面的类型表示中就可以使用它了
function identity<T>(arg: T): T {
  return arg;
}

// 可当作类型描述中的一部分使用
function identity<T>(arg: T[]): T[] {
  console.log(arg.length);
  return arg;
}

定义一个泛型接口

interface GenericIdentityFn {
  <T>(arg: T): T;
}

// 接口内共享同一个泛型变量
interface GenericIdentityFn<T> {
  (arg: T): T;
  myArr: T[];
}

定义一个泛型类

  • 泛型类指的是实例部分的类型,所以静态属性不能用泛型。
class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let emtity = new GenericNumber<number>();

泛型约束

  • 泛型作为类型的指代,故也可以理解泛型和接口可以组合使用,用来表示具有某些特定属性的泛型(泛型约束)

下例展示了限制只可接受带有length属性的数据(允许带有length属性的数据类型)

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

枚举

  • 使用枚举可以定义一组带名字的常量
  • ts支持数字和基于字符串的枚举
  • 使用enum进行定义

数字枚举

数字枚举具有自增长的性质, 会依据上一个成员的值自增得到下一个成员的值(默认第一个是0);并且可以反向映射,由枚举值得到枚举名字

enum Direction {
  Up = 1,
  Down, // 2
  Left, // 3
  Right // 4
}

enum Direction {
  Up, // 0
  Down, // 1
  Left, // 2
  Right, // 3
}

enum Enum {
  A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

function act(move: number, direction: Direction): void {
  console.log(move, direction);
}

act(10, Direction.Up);

// 无初始化的成员要么在首位,要么在明确被数值初始化了的成员之后
enum E {
  A = getSomeValue(),
  B, // Error!
}

字符串枚举

字符串枚举中的每个成员都必须进行显式的初始化

enum status {
  SUCC = "成功",
  FAIL = "失败",
}

const枚举

  • const枚举(常量枚举)不允许包含计算成员
const enum Directrions {
  Up,
  Down,
  Left, 
  Right
}

外部枚举

  • 外部枚举用来描述已经存在的枚举的形状
  • 外部枚举中无初始化方法时被当作需要进过计算的(区别于正常枚举中是当作常量成员)
declare enum Enum {
  A = 1,
  B,
  C = 2
}

变量声明

  • let和const是es6规范中新增的变量声明语法,因为ts是js的超集,所以其本身就支持二者,并且推荐在ts中使用。

var,let,const

  • 下面简要介绍一下三种变量声明的特点。
关键字 作用域 说明 声明
var var作用域/函数作用域 在包含它的函数,模块,命名空间或全局作用域内任何文职被访问到 可重复声明覆盖
let 词法作用域/块作用域 变量在包含它们的块或for循环之外不能访问 不能在声明之前读写,不可多次声明
const 词法作用域/块作用域 变量在包含它们的块或for循环之外不能访问 不能在声明之前读写,不可多次声明,不可重复赋值

解构

  • 解构数组

    let arr = [1, 3];
    let [first, second] = arr;
    let [first, second] = [second, first];
    function fun([first, second]: [number, number]) {
      console.log(first, second);
    }
    fun(arr);
    let arr2 = [1, 2, 3, 4];
    let [a, ...rest] = arr2;
    let [, b, , d] = arr2;
    
  • 解构对象

    let obj = {
      a: 1,
      b: 2,
      c: 3,
    };
    let { a, b } = obj;
    let { first, ...rest } = obj;
    let { a: aaa, b: bbb } = obj;
    let { a: val1, b: val2 }: { a: number, b: number } = obj;
    function fun(obj: {a: number, b?: number}) {
      let { a, b = 2 } = obj;
      return a + b;
    }
    
  • 函数声明

    type C = { a: string, b?: number };
    function fun({ a, b }: C): void {
      console.log(a, b);
    };
    // 为参数对象属性设置默认值(无该属性时生效)
    function fun2({ a = "", b = 0 }: C): void {
      console.log(a, b);
    };
    // 为参数对象设置默认对象(无该对象时生效)
    function fun3({ a, b = 0 } = { a: "" }: C): void {
      console.log(a, b);
    };
    
  • 展开

    1. 对象,数组等(可迭代对象)可展开
    2. 展开会覆盖前面的同名属性
    3. 仅包含对象自身的可枚举属性(会丢失方法)
    4. ts中不允许展开泛型函数上的类型参数

类型推论

类型兼容性

高级类型

Symbols

迭代器和生成器

模块

命名空间