Typescript学习 - 基础篇

156 阅读9分钟

Typescript的运行环境

node、npm/yarn、typescript(用于编译成JS的cli)、ts-node(typescript+node,TS的运行时)

Typescript的类型

与JS一致的7种数据类型
  • number
  • string
  • boolean
  • symbol
  • null
  • undefined
  • object
字面量类型
type Teacher = "teacher"; // Teacher类型的变量只允许赋值为"teacher"
type Profession = "teacher" | "student"; // Profession类型的变量只允许赋值为"teacher"或"student"
const str = 'hello';
type Hello = typeof str; // typeof str的结果是'hello', 所以Hello是字面量类型'hello'的别名
枚举类型(及其反向映射)

枚举的语法:

enum Fruit {
  apple = 2, // 假如未指定,则默认从0开始递增。也可以给所有枚举值指定为字符串。
  banana,
  orange,
}
const num: number = Fruit.banana; // 3

反向映射:数字枚举时,既可以根据属性名获取枚举值,也可以根据枚举值获取属性名

const num: number = Fruit.banana; // 3
const name: string = Fruit[num]; // 'banana'
函数类型
function func1(arg1: string, arg2: number): boolean { // 定义ts函数
  return true;
}
const func2 = (arg1: string, arg2: number): boolean => true; // 定义ts函数并赋值给func2
const func3: (arg1: string, arg2: number) => boolean = (arg1, arg2) => true; // 声明类型并赋值
字典类型/接口/函数类型 interface

表达字典类型是interface最常用的使用场景:

interface A {
  a: number;
  b: boolean;
  c: string[];
  d(x: number, y: number): number;
  e: (x: number, y: number) => number;
}
const va: A = {
  a: 123,
  b: true,
  c: ['hello', 'ts'],
  d(x, y){
    return x + y;
  },
  e: (x, y) => x + y,
};

interface还具有表达接口的能力。所谓接口就是对对象结构的约定

interface B {
  a?: number; // ? 表示该属性并非必需
  readonly b: string; // readonly 表示该属性只可读不可写
  [propName: string]: any; // 索引签名
}
const vb: B = {
  b: 'hello ts',
}

额外属性检查:一个interface类型被赋值对象字面量时有个特殊情况,该字面量会被检查是否具有interface规定之外的额外属性。若要跳过这种情况,可以使用类型断言或不要直接赋字面量

interface Person {
  name: string;
}
const alice = { name: 'Alice', age: 18 };
const person: Person = alice; // 类型检查通过,因为被赋值的不是对象字面量,不会检查额外属性
const anotherPerson: Person = { // 类型检查报错,因为被赋值的是对象字面量,经过检查发现了额外属性age,它不存在于Person接口
  name: 'Bob',
  age: 17,
};

其实根据后面介绍的类型兼容,interface类型被赋值对象字面量时如果缺少属性也会不兼容,所以实际上上述情况下被赋值必须跟interface规定的结构完全一样,属性不能多也不能少

interface还可以用于描述函数类型

interface Func {
  (x: number, y: number): boolean;
}
const compare: Func = (x, y) => x > y;

interface是可以继承/多继承的

interface Shape {
  color: string;
}
interface PenStroke {
  penWidth: number;
}
interface Square extends Shape, PenStroke {
  sideLength: number;
}
const square: Shape = {
  color: 'red',
  penWidth: 12,
  sideLength: 100,
};
混合类型
// 比如可以用接口表示一个变量既是函数又可以作为对象
interface Counter {
    (start: number): string; // 函数接口
    interval: number; // 实例
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
交叉类型与联合类型
type CrossType = string & number; // 交叉类型。type关键字表示类型别名
type UnionType = string | number; // 联合类型
const c: CrossType = 123; // ts类型检查报错,因为123不能满足既是string又是number(实际上任何赋值都不能满足,所以CrossType实际上是never类型)
const u: UnionType = 123; // ts类型检查通过,因为UnionType只要是string或者number都可以满足

// 交叉类型一般用于merge两个字典类型从而得到新类型,不过如果存在冲突的话则冲突属性会变成never类型(无论如何赋值都会类型检查报错)
type ICrossType = A & B;

类型断言

  1. 尖括号形式
const oneStr: any = "this is a string";
const len: number = (<string>oneStr).length;
  1. as形式
const oneStr: any = "this is a string";
const len: number = (oneStr as string).length;

以上两种断言效果等价,但是在tsx文件中只能使用as形式,大概是因为尖括号在tsx中有特殊的含义(表示组件实例)

类型保护

如果一个值是交叉类型,我们可以访问各子类型的所有属性;但如果一个值是联合类型,我们只能访问各子类型共有的属性,所以就会出现下面的情况:

interface Student {
  learn(): void;
}
interface Teacher {
  teach(): void;
}
type Person = Student | Teacher;
function doSomething(person: Person): void {
  if (person.learn !== undefined) {
    person.learn(); // 类型检查报错, 因为learn并不是Student和Teacher类型的共有属性
  }
}

虽然代码符合逻辑,能够正常执行,但是为了避免类型检查报错,我们可以采取“类型保护”措施,比如:

  • 类型断言
function doSomething(person: Person): void {
  if (person.learn !== undefined) {
    (person as Student).learn(); // 将person断言为Student类型就能使用learn属性了
  }
}
  • 类型谓词
function isStudent(person: Person): person is Student {
  return (person as Student).learn !== undefined; // 输入Person类型的值,返回布尔值,但注意返回类型是主谓宾结构
}
function doSomething(person: Person): void {
  if (isStudent(person)) { // ts会根据谓词得知person为Student类型,这样就允许使用learn属性了
    person.learn(); 
  }
}
  • 使用typeof或者instanceof进行类型判断

利用这两个关键词对类型进行判断,作用跟类型谓词是一样的,只是写起来更简单。typeof只能判断基本类型,instanceof可以判断构造函数

class Student {
  constructor(){}
  learn(){}
}
class Teacher {
  constructor(){}
  teach(){}
}
type Person = Student | Teacher;
function doSomething(person: Person): void {
  if (person instanceof Student) {
    person.learn();
  }
}

类型兼容

某个变量已经声明为某种类型(或者隐含地被TS编译器推导为某种类型)之后,再被赋值时必须兼容该类型才能类型检查通过。一般来说只有以下情况是兼容的:

  1. 声明类型包含了赋值类型,比如 let a: string; a = 'hello'这句里a的类型是string, 它是所有字面量类型的集合,而'hello'的类型是字面量类型'hello',所以前者包含了后者
  2. 赋值类型与声明类型完全一致(其实可以归结为1的情况)
  3. 声明类型和赋值类型都是interface,且赋值类型不是字面量(无额外属性检查)且后者包含前者规定的所有属性(繁赋简)(其实可以归结为1的情况)
  4. 声明类型和赋值类型都是函数,且前者的函数签名包含后者的所有形参(简赋繁)

类型推断

当初始化某个变量时,如果没有指定其类型,则TS编译器会根据赋值自动推断出其类型

let v = 123; // TS推断为number类型
v = 'hello'; // Type '"hello"' is not assignable to type 'number'

// 用interface契约来约束class的结构
interface ClockInterface {
  currentTime: Date; // 约束实例属性或方法
  setTime(time: Date): void; // 约束原型方法
}
class Clock implements ClockInterface {
  currentTime: Date;
  setTime(time: Date) {
    this.currentTime = time;
  }
  constructor(){}
}

// 其它约束
class Employee {
  static company = "bat"
  readonly code: string; // 只读属性
  private _fullName: string;
  get fullName() { // 类存取器, 约束类的原型属性
    return this._fullName;
  }
  set fullName(name: string) { // 类存取器, 约束类的原型属性
    this._fullName = name;
  }
}

// 类的继承
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
  move(n: number) {
    console.log(`${this.name} moves ${n} meters`);
  }
}
class Snake extends Animal {
  constructor(name: string) {
    super(name); // 这里的super表示基类构造函数,在子类的构造函数里访问this之前必须先调用super
  }
  move(n: number) { // 覆盖基类的同名方法
    super.move(n); // 这里的super表示基类的原型
    console.log('Snake');
  }
}

// 抽象类:只能作为基类使用的类,不会被直接实例化;与接口(约束结构)不同的是它可以包含成员的实现细节
abstract class Base {
  abstract makeSound(word: string): void; // abstract关键字修饰的表示抽象方法,它不包含具体实现但要求派生类中必须实现,这一点与接口类似
  move(len: number): void {
    console.log(`${len} meters moved`);
  }
}
class Son extends Base {
  makeSound(word: string){
    console.log('yell');
  }
}

类成员的访问修饰符

// 类成员的默认修饰符为public,表示可以自由访问
// private修饰符表示只能在类定义体内部访问
// protected修饰符表示只能在类定义体内部或者派生类中访问

// 构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,
class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}
class Employee extends Person { // Employee 能够继承 Person
    private department: string;
    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }
    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

// 修饰符兼容性举例
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"); 
rhino = animal
rhino = employee // error

类具有两种类型:静态类型(类函数/构造器本身的类型),实例类型(类实例化以后具有的类型),前面我们说的类型都是后者。如果要描述静态类型可以这样:

interface InstanceInterface { // 实例类型
  name: string;
  age: number;
}
interface ConstructorInterface { // 静态类型,类似于函数接口
  new (name: string, age: number): InstanceInterface;
}

类可以当作接口使用

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

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

泛型

泛型就是高阶类型(概念类比于高阶函数、高阶组件),是一种类型生成器,我们通过向其传入参数来输出实际类型

下面介绍的泛型函数,其参数是运行时指定的;而泛型类型和泛型类,其参数则是在代码里手动声明

泛型函数

基础写法

function hello<T>(arg: T): T {
  return arg;
}
const hello = <T>(arg: T): T => arg;

之前说过interface也可以表述函数类型,所以泛型函数也可以这么写

interface Func {
  <T>(arg: T): T;
}
const func: Func = arg => arg;

// 或如下,花括号{}就代表了interface
const func: { <T>(arg: T): T } = arg => arg;

使用泛型函数的两种方法:

  • 用类型断言指定类型:const temp = hello<string>('hello world')
  • 利用类型推断,让TS运行时根据传入的参数类型自动确定T的类型:const temp = hello('hello world');
泛型类型
type Type<T> = T;
type NumberType = Type<number>; // number

// 函数类型也可以用泛型类型来表达
// ⚠️注意这里表示的是泛型的函数类型,而不是泛型函数
interface GenericIdentityFn<T> {
    (arg: T): T;
}
const myIdentity: GenericIdentityFn<number> = arg => arg;
泛型类
// 泛型类与泛型接口的定义方式一致
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
泛型约束

指的是对泛型参数施加的限制条件

// 约束了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;
}

// 约束了K必须是T的属性之一
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

// 工厂函数:约束了泛型参数T必须是类c的实例类型
// 注意此处c的类型{new(): T; }表示它是一个类,且其实例类型为T
function create<T>(c: {new(): T; }): T {
    return new c();
}