前端猛男带你玩转Typescript(上)

avatar
前端团队 @晓教育集团

随着前端技术的不断发展,typescript像一头洪水猛兽一般涌进了前端工程师的领域,同时也给javascript注入了新的生命力。接下来笔者将会从以下几个方面去了解typescript的特点以及它的基础语法。

一、为什么要使用typescript

在开始讲述typescript的语法之前,笔者觉得有必要对typescript的诞生背景进行一定的描述,而这一切都要从编程语言的类型说起。 现有主要的编程语言分为:强类型或者弱类型语言、静态类型或者动态类型。

强类型与弱类型

强类型语言,指的是强类型定义语言,不允许任意的隐式类型转换。 弱类型语言,指的是数据类型可以被忽略的语言,允许任意的数据隐式类型转换。

静态类型与动态类型

静态类型语言,指的是变量声明过后,它的类型就不允许再修改。 动态类型语言,指的是变量在运行阶段才能明确变量类型,而且变量的类型可以随时改变。

使用typescript的原因

从上面的分类可以知道,javascript是一门动态弱类型语言,是一门‘任性’并且‘不走寻常路’的语言。而typescript作为javascript的超集,它更加贴近静态强类型语言,能够为我们的编程带来以下的好处: 1、错误更早的暴露 2、代码更智能,编码更准确 3、重构更牢靠 4、减少不必要的类型判断 下面就让我们一起进入typescript的世界!!!

二、typescript语法

这个章节主要从typescript的数据类型、typescript的高级用法、typescript的模块和声明这三个部分进行讲述。

typescript的数据类型

TypeScript支持与JavaScript几乎相同的数据类型,所以我们从ECMAScript 6(简称ES6)的数据类型开始入手。 ES6的数据类型包含有: 基础类型:number、string、boolean、symbol、null、undefined; 复杂类型:object(包含function,array)

以上代码将ES6相关的数据类型种类打印出来,控制台结果如下:

那在typescript中对应的数据类型如下:

1、number -> number

和javascript一样,typescript里面所有的数字都是浮点数,同时支持二进制、八进制、十进制和十六进制的字面量

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

2、string -> string

typescript支持双引号(")、单引号(')以及反引号(`)表示字符串

let one: string = 'abc';
let two: string = "efg";
let three: string = `${one}-${two}`;

3、boolean -> boolean

typescript支持boolean的true和false

let right: boolean = true;
let wrong: boolean = false;

4、null -> null 和 undefined -> undefined

TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null ,默认情况下null和undefined是所有类型的子类型 ,但是当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自

let u: undefined = undefined;
let n: null = null;

5、array

TypeScript像JavaScript一样可以操作数组元素,共有三种方式可以定义数组:

(1)元素类型 [ ]

let list: number[] = [1, 2, 3];

(2)Array<元素类型>

let list: Array<number> = [1, 2, 3];

(3)元组Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let list: [string, number] = ['123', 123];

当访问一个已知索引的元素,会得到正确的类型:

console.log(list[0].substr(1)); // OK
console.log(list[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

list[2] = 456; // ok,数字可以赋值给之前定义的 string 或者number 类型, 其他的则不行

6、function

(1)在typescript里的函数,可以给每个参数添加类型之后再为函数本身添加返回值类型

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

(2)在typescript里的函数,只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确

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

(3)在typescript里的函数,实参和形参的数量必须一致(在没有使用可选参数、默认参数或者剩余参数的情况下)

function buildName(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");         // ah, just right

(4)在typescript里的函数,使用?实现可选参数的功能,可选参数必须跟在必须参数之后

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}
let result1 = buildName("Bob");  // works correctly now
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");  // ah, just right

(5)在typescript里的函数,可以为参数设置默认值,在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略 。注意:默认值和可选参数不能作用在同一个参数上

function buildName(firstName: string, lastName: string = "Smith"): string {
    return firstName + " " + lastName;
}
let result1 = buildName("Bob");                  // works correctly now, returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result4 = buildName("Bob", "Adams");         // ah, just right

(6)在typescript里的函数,可以设置剩余参数来接收0个或者多个参数,用法为'...'加上参数数组

function buildName(firstName: string, ...restOfName: string[]): string {
  return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

7、object

(1)直接使用object来声明,object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

let value: object = {}; // ok
value = 123; // error,

(2)使用{}对象结构

let value: {first: string, second: number} = {
  first: 'abc',
  second: 123,
}

(3)使用interface接口来定义,在后面的高级用法会进行具体的讲解

8、enum(枚举)

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

(1)数字枚举

如果不使用初始化器,枚举的第一个值为0,其余成员如果没有赋值,它的值为上一个枚举成员的值加1

enum Direction {
    Up = 1,
    Down,
    Left = 10,
    Right
} // Right的值为Left的值加1,为11

(2)字符串枚举

在一个字符串枚举里,每个成员都必须用字符串字面量

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

(3)异构枚举

枚举可以混合字符串和数字成员

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

(4)枚举做了什么

我们可以从ts编译后的js文件去更好的理解枚举,以下面的枚举为例: typescript编译为javascript后,可以清楚的看到枚举实际为创建一个可以双向引用的对象 这一点从console的枚举打印也可以验证

9、any

有时候我们并不能确定变量的类型,为了不让类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;

建议:不要通盘使用any

在javascript过渡到typescript的过程里面,很多人都是直接使用any来进行变量类型声明,网友们经常戏称这种做法为'anyscript',这样固然没有问题,但是这破坏了typescript诞生的初衷,并没有向静态强类型语言靠拢。我们应该更多的确定变量的类型,或者使用联合类型(后面的高级用法会阐述这方面的知识),让我们的代码变得更加的智能。

10、void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function warnUser(): void {
    console.log("This is my warning message");
}

11、never

never类型表示的是那些永不存在的值的类型 ,它可以是以下三种值:

(1)抛出error异常

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

(2)不会有返回值的函数表达式或箭头函数表达式的返回值类型

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

(3)被永不为真的类型保护所约束的变量

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

typescript的高级用法

前面的章节介绍了typescript的一些基本类型,对于简单类型的声明相信大家已经信手拈来。但是在日常的开发中,复杂对象类型才是代码中的主角,typescript对其进行了许多的改造:例如使用接口(interface)对对象结构进行检查;对es6中新增的class类型进行更加深度的改造,使得更加靠近常见的静态语言;还有使用泛型来增加可重用性等等。下面就让我们来领略一下它们的风采。

1、接口(interface)

interface一般是为复杂object类型提供结构类型检查,上面的例子也有提及,下面就让我们来改造一下

// 接口名使用大驼峰写法,变量声明之间使用分号(;)分割
interface ValueOne {
  first: number;
  second: string;
}
let valueOne: ValueOne = {
  first: 123,
  second: '123',
}
let valueTwo: {first: number, second: string} = {
  first: 123,
  second: '123',
}

valueOne和valueTwo的声明具有相同的作用,但是interface可以为多个变量复用;同时有一点需要注意的,interface只提供类型检查,不提供具体实现

(1)可选属性

interface可以跟之前的function一样定义一些可选属性,但有一点要注意的,不能传入未经定义的键名(除非使用类型断言,但不推荐这么使用),例子如下:

interface CommonValue {
  first?: number;
  second?: string;
}
let valueOne: CommonValue = {
  first: 123,
}
let valueTwo: CommonValue = {
  second: '123',
}
let valueThree: CommonValue = <CommonValue>{
  third: 123,
} // 会报错

(2)只读属性

interface中可以对一些属性设置只读属性,只能在对象刚创建的时候修改其值,例子如下:

interface CommonValue {
  first?: number;
  second?: string;
  readonly third: boolean;
}

let valueOne: CommonValue = {
  first: 123,
  third: true,
}

valueOne.third = false; // error

(3)可以用来声明函数

在javascript中function也是对象的一种,因此interface可以用来声明function也是正常的,function的声明一般只包含参数列表以及返回值,其余用法与上面的function一样,例子如下:

interface CombineNames {
  (first: string, second: string, ...res: Array<string>): string;
}

let getName: CombineNames = (one: string, two: string, ...rest: Array<string>): string => {
  let result: string = `${one}-${two}-${rest.length?rest[0]: ''}`;
  return result;
}

console.log(getName('abc', 'def', 'hello', 'what'));

(4)接口里的函数

在interface里可以声明某一个属性为function,它由属性名+参数列表+返回值,用法如下:

interface ValueSet {
  value: string;
  getValue(value: string): string;
  getLowerCaseValue: (value: string) => string;
}

let valueTwo: ValueSet = {
  value: 'ABC',
  getValue(value: string) {
    return value
  },
  getLowerCaseValue(value: string) {
    return value.toLowerCase();
  }
}

(5)索引签名

在javascript中,我们经常可以使用对象键名作为索引去查询对应的值,而在interface中,支持两种索引类型:number和string。例子如下:

interface IndexSetting {
  [index: number]: string; // index名称可以任意定义
}

let valueTwo: IndexSetting = ['123', '456'];

但是有两点是我们需要注意的: 1)索引的类型只能是number或者string,其他类型都不行 2)可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型或者其子类型。

interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
  [x: number]: Animal;
  [x: string]: Dog;
}

// 正确
interface Okay {
  [x: number]: Dog;
  [x: string]: Animal;
}

3)当索引签名与其它属性名公用时,interface里面所有值类型必须相同

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

(6)混合类型

有时你会希望一个对象可以同时具有多种类型。例如,一个对象可以同时做为函数和对象使用,并带有额外的属性,那么你可以这么做:

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;

(7)接口之间的继承

有的时候,多个接口具有共有部分,我们可以使用继承extends的方法把共有部分抽离出来,尽量的简化代码,例子如下:

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
} // 一个接口可以继承多个接口 

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

2、类(class)

在开始介绍typescript的类之前,我们先来看一下es6中的类的特点: 在es6规范中引入了class的概念,使得js开发者告别了原型对象模仿面向对象中的类和类继承的模式,但是js中并没有一个真正的class原始类型,class仅仅只是对原型对象的语法糖。下面来看一个简单的例子

class Test {
  value: string;
  constructor(value: string) {
    this.value = value;
  }
  getValue() {
    return this.value;
  }
}

它等同于原型对象这样的写法:

function Test(value) {
        this.value = value;
    }
Test.prototype.getValue = function () {
    return this.value;
};

在es6的class中,还使用static定义静态方法或者属性,extend来继承父类,具体语法可以参考es6的官方文档,这里不展开描述。 那typescript的类又做了什么呢? 首先typescript兼容es6中类的所有写法,然后还新增了公共、私有、受保护以及只读的修饰符,即public、private、protected和readOnly,对于经常使用面向对象方式的程序员来说又是那种熟悉的味道。

(1)public

在TypeScript里,成员都默认为 public ,表示可以该字面量可以被自由访问。当然你也可以明确的将每一个成员标识为public。

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

(2)private

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

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

new Animal("Cat").name; // 错误: 'name' 是私有的.

(3)protected

protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问 。

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} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

(4)readOnly

你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

3、泛型

跟C#和Java这样的语言一样,typescript使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 我们先来看一下下面的例子,假如我们不确定传入的参数类型,很多人都会用下面的做法:

function init(value: any): any {
  return value;
} 
let initValue: number = init(123);

这种做法比较粗暴,我们来看看如何利用泛型来处理:

function init<T>(value: T): T {
  return value;
}

let initValue: number = init<number>(123); // 明确类型
let value: string = init('123'); // typescript会进行类型推断

(1)泛型接口

我们现在将泛型和interface结合起来:

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

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

(2)泛型类

我们现在将泛型和class结合起来:

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; };

(3)泛型约束

假如我们在类型未确定之前,想要使得类型含有某些属性,按照以下写法是会有问题的:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

这个时候我们可以使用extends来进行约束,这点很重要,将会在可重用接口上大派用场:

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;
}

4、类和接口的相爱相杀

(1)class implements interface

上面提到了interface只是提供类型检查,那么它是否可以拿来约束class呢?答案是肯定的。而class是使用implements来强制接受interface约束:

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

在angular的t组件s文件中,也是使用interface来约束生命周期方法的必须实现,一个class同样可以获取多个interface约束:

export class ClassReportComponent implements OnInit, AfterViewInit {
  ngOnInit() {}
  ngAfterViewInit() {}
}

(2)interface extends class

那么反过来,interface可以继承class吗?答案也是肯定的,当接口继承了一个类类型时,它会继承类的成员但不包括其实现,这看起来就像接口声明了所有类中存在的成员的类型,但是没有提供具体实现。

class Control {
  state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button implements SelectableControl {
  state: any = '';
  select() {};
}

在这里需要注意的是,接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现。例子如下:

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button extends Control implements SelectableControl {
  select() {};
}

(3)abstract class

抽象类等同于interface和class的结合体,它有以下几个特点: 1)抽象类可以包含成员的实现细节,但是不能直接实例化(new)

abstract class Department {
  constructor(public name: string) {
  }

  printName(): void {
      console.log('Department name: ' + this.name);
  }
}

let a: Department = new Department(); // 错误: 不能创建一个抽象类的实例 

2)抽象类中的抽象方法不包含具体实现并且必须在派生类中实现

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

5、类型兼容性

TypeScript里的类型兼容性是基于结构子类型的,也就是说是否兼容要看两个变量的属性值是否符合检查规则。在这里主要是对复杂类型的兼容性展开讨论

(1)对象类型

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性,即y的属性类型需要包含x的属性 类型。例子如下:

interface Named {
    name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y; // ok,y中含有name属性,且为string类型

(2)函数

判断两个函数是否兼容,在这里我们仅介绍只有参数列表不同的情况,因为其他情况使用场景较少,有兴趣的朋友可以上typescript的官网查看。例子如下:

let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error, because x() lacks a location property

(3)类

比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。例子如下:

class TestOne {
  static type: string = '123';
  value: number;
  constructor(value: number) {
    this.value = value;
  }
}


class TestTwo {
  static source: number = 123;
  value: number;
  constructor(value: number) {
    this.value = value + 123;
  }
}


let testOne: TestOne;
let testTwo: TestTwo;


testOne = testTwo;
testTwo = testOne;

在这里需要注意的是类里面有可能会存在私有和受保护成员,这时候的兼容规则是: 如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员

class TestOne {
  private count: number;
  static type: string = '123';
  value: number;
  constructor(value: number) {
    this.value = value;
  }
}


class TestTwo {
  static source: number = 123;
  value: number;
  constructor(value: number) {
    this.value = value + 123;
  }
}


let testOne: TestOne;
let testTwo: TestTwo;


testOne = testTwo; // error
testTwo = testOne; // correct

6、联合类型及其使用

(1)类型断言

在开始介绍联合类型之前,我们先来了解一下typescript的类型断言。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。它有两种形式: 1)尖括号语法

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

2)as语法

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

(2)联合类型的概念

我们在开发中经常会遇到这种情况,一个变量可能会有多个类型的值,例如后端返回的结构有时是number,有时是string,又或者一个表单属性选择中,单选项值可能为string,多选项值可能是一个有多个string组成的数组,因此在开发的角度来说,我们无法确定值类型,而又不想采用any的形式,这个时候联合类型就会派上用场。例子如下:

let value: Array<number | string> | number | string;
// value的值可能多种

但是滥用联合类型也会带来很多问题,下面我们引入两个例子

(3)类型保护*

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface Father {
  sweepFloor(): void;
  makeMoney(): void;
}


interface Mother {
  cook(): void;
  makeMoney(): void;
}


function isWho(): Father | Mother {
  return;
}


let person = isWho();
person.makeMoney(); // ok
person.cook(); // error

  但是需求就是要根据不同类型执行不同的方法,那我们可以怎么做呢?

interface Father {
  sweepFloor(): void;
  makeMoney(): void;
}


interface Mother {
  cook(): void;
  makeMoney(): void;
}


function isWho(): Father | Mother {
  return;
}


let person = isWho();
person.makeMoney(); // ok


if((<Mother>person).cook) {
  (<Mother>person).cook();
} else if((<Father>person).sweepFloor) {
  (<Father>person).sweepFloor();
}

上面的例子使用类型断言来强制执行对应的方法,但这个方法有个弊端,就是每次使用都要进行类型断言,这样未免太麻烦了。那么我们可不可以先一次声明好变量,后面这个变量就不用再声明了。这个时候就可以使用typescript的类型保护来进行处理

interface Father {
  sweepFloor(): void;
  makeMoney(): void;
}

interface Mother {
  cook(): void;
  makeMoney(): void;
}

function isWho(just: boolean): Father | Mother {
  let father: Father = {
    sweepFloor: () => {},
    makeMoney: () => {},
  }
  let mother: Mother = {
    cook: () => {},
    makeMoney: () => {},
  }
  return just? father: mother;
}

function isFather(person: Father | Mother): person is Father {
  return (<Father>person).sweepFloor !== undefined;
}

function isMother(person: Father | Mother): person is Mother {
  return (<Mother>person).cook !== undefined;
}

let xiaoMing = isWho(true);

if(isFather(xiaoMing)) {
  xiaoMing.sweepFloor();
} else if(isMother(xiaoMing)) {
  xiaoMing.cook();
}

除了上述的写法,typescript还支持typeof以及instanceof的类型保护,有兴趣的可以自己尝试一下。

(4)求仁得仁*

假如现在又有一个需求,参数类型为string或者number,根据入参的类型返回对应参数的值,很多人这个时候就会想到函数重载和联合类型的结合:

function double(x: number | string): number | string;
function double(x: any) {
  return x + x;
}

但是结果却不尽如人意

// const num: string | number
const num = double(10); 
// const str: string | number
const str = double('ts');

返回结果仍然是联合类型,后面的使用仍然要进行类型断言或者类型保护 这个时候我们就可以使用泛型+泛型约束的方法来处理:

function double<T extends number | string>(x: T): T;
function double(x: any) {
  return x + x;
}

7、类型别名

类型别名,顾名思义,会给一个类型起个新名字,同时也意味着它可以作用于原始值,联合类型,元组以及其它任何需要你手写的类型

(1)基础类型

type a = number;
type b = string;
type c = boolean;
type d = symbol;
type e = null;
type f = undefined;
type g = any;
type h = never;

(2)复杂类型

在前面提到的复杂类型在这里都可以用type别名来表示

type a = () => number;
type b = Array<number>;
type c<T> = {
  name: string;
  height?: number;
  readonly value: T;
}

(3)可在属性里引用自己

相信大家会经常在项目中使用嵌套树状结构,这个时候type这个特点就可以派上用场

type Tree<T> = {
  id: number;
  value: T;
  children?: Array<Tree<T>> | null;
}
let tree: Tree<number> = {
  id: 123,
  value: 123,
  children: [
    {id: 456, value: 456, children: null}
  ]
}

当然,其实interface也可以实现同样的效果,所以我们可以根据自己的实际情况进行选择

interface Tree<T> {
  id: number;
  value: T;
  children?: Array<Tree<T>> | null;
}
let tree: Tree<number> = {
  id: 123,
  value: 123,
  children: [
    {id: 456, value: 456, children: null}
  ]
}

但是也要注意一点,就是类型别名不能出现在声明右侧的任何地方

type Yikes = Array<Yikes>; // error

(4)字符串字面量

字符串字面量允许你指定字符串必须的固定值,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。

type Subject = 'math' | 'chinese' | 'english';
function selectSubject(val: Subject) {
  val == 'math' && console.log(`select ${val}`);
  val == 'chinese' && console.log(`select ${val}`);
  val == 'english' && console.log(`select ${val}`);
}

(5)数字字面量

同样的,type还有数字字面量的形式

type Num = 1 | 2 | 3 | 4 | 5;

(6)映射类型

有的时候我们希望获取一个只读类型的映射表,按照前面的方法要在对象每个键值对前面加上readonly字样,这样会显得有点麻烦和臃肿,这时候就可以使用keyof结合type的方式去实现

type ReadonlyItem<T> = {
  readonly [key in keyof T]: T[key];
}
type PartialItem<T> = {
  [key in keyof T]?: T[key];
}
interface Person {
  name: string;
  age: number;
}
let d: ReadonlyItem<Person> = {
  name: 'james',
  age: 18,
}
d.age = 23; // error
let e: PartialItem<Person> = {
  name: 'kobe',
} // correc

END

关于我们:我们是晓教育集团大教学前端团队,是一个年轻的团队。我们支持了集团几乎所有的教学业务。现伴随着事业群的高速发展,团队也在迅速扩张,欢迎各位前端高手加入我们~ 我们希望你是:技术上基础扎实、某领域深入;学习上善于沉淀、持续学习;性格上乐观开朗、活泼外向。如有兴趣加入我们,欢迎发送简历至邮箱: