30分钟快速掌握TypeScript

386 阅读13分钟

本文是学习《TypeScript入门教程》时做的笔记。对typescript知识进行了梳理重组,希望本文能带你轻松入门。

一. TypeScript的类型定义

布尔值

使用boolean定义布尔值类型

let isDone: boolean = false;

数值

使用number定义数值类型

let num:number = 6;

字符串

使用string定义字符串类型

let myName:string = 'Joy'

字符串字面量类型

字符串字面量类型,用来约束取值只能是某几个字符串中的一个。字符串字面量类型使用type进行定义。

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'

枚举类型

枚举使用enum关键字来定义

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

空值

用void表示没有任何返回值的函数

function alertName():void{
    alert('myName is Joy')
}

声明一个void类型的变量没有什么作用,只能将他赋值为undefined和null

Null和Undefined

使用null和undefined来定义这两个原始类型

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

null和undefined是所有类型的子类型,也就是说null和undefined类型的变量,可以赋值给其他类型的变量

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

let str1:string = n;
let str2:string = u;
let str3:void = n;
let str4:void = u;

console.log(str1)//输出null
console.log(str2)//输出undefined
console.log(str3)//输出null
console.log(str4)//输出undefined

undefined和null的区别

undefined 表示一个变量自然的、最原始的状态值,这种原始状态会在以下4种场景中出现。

    1. 声明一个变量,但是没有赋值。
    1. 访问对象中不存在的属性或者未定义的属性。
    1. 函数定义的形参,但是没有传递实参。
    1. 使用void操作符,对表达式求值。

null 表示一个变量被人为的设置为空对象,而不是原始状态。以下两种情况,我们会将变量赋值为null。

    1. 如果定义一个变量,在将来用于保存对象。那么最好将该变量初始化为null。
    1. 当一个数据不再使用时,将其设置为null来释放引用。

任意值

任意值(any)用来表示允许赋值为任意类型,在任意值上访问任何属性都是允许的。

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

如果在声明变量的时候,未指定其类型,那么它会被识别为任意类型。

let something;
something = 'seven';
something = 7;

something.setName('Tom');

等价于

let something:any;
something = 'seven';
something = 7;

something.setName('Tom');

内置对象类型

javascript有许多内置对象,他们可以直接在TypeScript中当做定义好的类型。

  1. ECMAScript提供的内置对象有:Boolean、Error、Date、RegExp等。我们可以在TypeScript中将变量定义为这些类型。
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

注意:基本类型和内置对象的区别。在 TypeScript 中,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。使用new Boolean()返回的是一个Boolean对象,直接调用 Boolean 返回一个 boolean 类型。其他基本类型也一样。

let createdByNewBoolean: Boolean = new Boolean(1);
let createdByBoolean: boolean = Boolean(1);
  1. DOM和BOM的内置对象有:DocumentHTMLElementEventNodeList 等。
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});

联合类型

联合类型使用 | 分割每个类型,表示取值可以为多种类型中的一种。下面代码,允许 myFavoriteNumber 的类型是string或者number,但是不能是其他类型。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
myFavoriteNumer = true;//编译时报错

当不能确定一个联合类型的变量到底是那个类型时,我们只能访问此联合类型的所有类型的共有属性和方法。

function getLength(something: string | number): number {
    console.log(something.length);//length 不是 string 和 number 的共有属性,所以会报错
    console.log(something.toString());//访问 string 和 number 的共有属性是没问题的
}

当联合类型被赋值时,会推断出一个类型。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // myFavoriteNumber 被推断成了 string
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // myFavoriteNumber 被推断成了 number,访问length属性时报错

数组类型

数组类型有多种定义方式

  1. elemType[]表示法
let fibonacci: number[] = [1, 1, 2, 3, 5];
  1. 数组泛型,用Array 来表示数组
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
  1. 用接口表示数组
interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

元组类型

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

let tom: [string, number];
tom = ['Tom', 1];
console.log(tom[0].length)//3
console.log(tom[1]+5)//6
tom[0]=2//
tom.push(1)//[ 'Tom', 1, 1 ]
tom.push('aaa')//[ 'Tom', 1, 1 ]

两点说明:

  1. 访问元组元素或给元素赋值时,能够推断出正确的类型。
  2. 当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型

函数类型

函数的定义需要把输入和输出都考虑到。

1. 函数声明的类型定义

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

2. 函数表达式的类型定义

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

上面的代码只对匿名函数进行了定义,等号左边的mySum,是通过赋值操作进行类型推论而推断出来的。手动给mySum添加类型,应该是这样

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

3. 用接口定义函数形状

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

采用函数表达式和接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

4. 函数的参数

  • 可选参数:用?表示可选参数,可选参数应该放在必需参数的后面。
function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
  • 参数默认值
function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
  • 剩余参数:可以使用 ...rest 的方式获取函数中的剩余参数,rest 参数只能是最后一个参数。
function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

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

5. 函数的重载

我们可以使用重载定义多个函数类型

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

上例中,我们重复定义了多次函数reverse,前几次都是函数定义,最后一次是函数实现

接口

TypeScript中接口非常灵活,可以用接口定义数组和函数形状,还可以用接口对【对象的形状(Shape)】进行描述,对类的一部分进行抽象。

1. 对象形状的描述

  • 变量的形状必须和接口保持一致

定义的变量,比接口少了一些属性或多一些属性都是不允许的。下面的例子中,tom1和tom2编译时会报错。

interface Person {
    name: string;
    age: number;
}

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

let tom1: Person = {//error 不允许少添加属性
    name: 'Tom',
};

let tom2: Person = {//error 不允许添加未定义的属性
    name: 'Tom',
    age: 25,
    gender: 'male'
};
  • 可选属性

当我们希望不要完全匹配一个形状,可以使用可选属性。此时仍然不允许添加未定义的属性。

interface Person {
    name: string;
    age?: number;//可选参数
}

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

let tom1: Person = {
    name: 'Tom',
};

let tom2: Person = {//error 不允许添加未定义的属性。
    name: 'Tom',
    age: 25,
    gender: 'male'
};
  • 任意属性

使用[propName:string]: typeName定义任意属性。

注意:一旦定义了任意属性,那么确定属性和可选属性的类型,必须是他的类型子集。

如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

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

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};
  • 只读属性

如果希望对象中的一些字段只能在创建时被赋值,那么可以用readonly定义只读属性。

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:

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

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

tom.id = 9527;//error  使用 readonly 定义的属性 id 初始化后,又被赋值了,所以报错了。

2. 类的抽象

  • 类实现接口
interface Alarm {
    alert(): void;
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}

class Car implements Alarm {
    alert() {
        console.log('Car alert');
    }
}

一个类可以实现多个接口

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');
    }
}
  1. 接口继承接口

接口与接口之间可以是继承关系:

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}
  1. 接口继承类

将类看作是类的类型,Point类的Point类型只包含实例属性和方法。因此,Point和PointInstanceType是等价的。

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

泛型

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

1. 泛型函数

我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

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']

也可以一次定义多个类型参数,下面代码定义了一个swap函数,用来交换输入的元组。

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

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

2. 泛型接口

interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}

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

createArray(3, 'x'); // ['x', 'x', 'x']
interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

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

createArray(3, 'x'); // ['x', 'x', 'x']

3. 泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

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

4. 泛型约束

我们可以对泛型进行约束,如下代码,只允许这个函数传入那些包含length属性的变量。

interface Lengthwise {
    length: number;
}

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

5. 泛型参数默认类型

我们可以为泛型中的类型参数指定默认类型

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

二. 类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。类型断言只能够「欺骗」TypeScript编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误

类型断言的语法

as 类型

<类型>

推荐使用,值as类型语法。

类型断言的用途

  1. 将一个联合类型断言为其中一个类型。

当不能确定一个联合类型的变量到底是那个类型时,我们只能访问此联合类型的所有类型的共有属性和方法。下面例子,获取animal.swim 的时候会报错。

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

function isFish(animal: Cat | Fish) {
    if (typeof animal.swim === 'function') {//获取 animal.swim 的时候会报错。
        return true;
    }
    return false;
}

然而,有时候确实需要在还不确定类型的时候就访问其中一种类型特有的属性或方法,此时可以使用类型断言。将 animal 断言成 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;
}
  1. 将一个父类断言为更加具体的子类
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;
}
  1. 将任何一个类型断言为 any

在 any 类型的变量上,访问任何属性都是允许的。

当我们非常确定这段代码不会出错,而typescript给出了相应的错误提示,这时可以将类型断言为any。

下面代码,在window对象中添加一个属性foo,typescript编译时会报错。

window.foo = 1;

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

(window as any).foo = 1;
  1. 将any断言为一个具体的类型

通过把any断言为精确的类型,使我们的代码向着高可维护的目标发展,提高代码的可维护性。

  1. 兼容的类型相互断言

若 A 兼容 B,那么 A 和 B 可以互相进行类型断言。A 兼容 B可以理解为A中的属性或方法完全包含B。他们的最终结构可等价为A extends B。

如下代码,Cat 包含了 Animal 中的所有属性,除此之外,它还有一个额外的方法 run。Cat和 Animal类型可以互相断言。

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

function testAnimal(animal: Animal) {
    return (animal as Cat);
}
function testCat(cat: Cat) {
    return (cat as Animal);
}

综上所述:

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型
  • 要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可

其实前四种情况都是最后一个的特例。

类型断言VS类型转换/类型声明/泛型

  1. 类型断言VS类型转换

断言类型只会影响TypeScript编译时的类型,类型断言语句在编译结果中会被删除。断言类型不会真的影响到变量的类型。

function toBoolean(something: any): boolean {
    return something as boolean;
}

toBoolean(1);// 返回值为 1

若要进行类型转换,需要直接调用类型转换的方法。

function toBoolean(something: any): boolean {
    return Boolean(something);
}

toBoolean(1);// 返回值为 true
  1. 类型断言VS类型声明

类型声明是比类型断言更加严格。如下代码所示。

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

const animal: Animal = {
    name: 'tom'
};
//类型声明,Animal 可以看作是 Cat的父类,不能将父类的实例赋值给类型为子类的变量,因此会编译报错。
let tom: Cat = animal;
//类型断言,由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom。
let tom = animal as Cat;

所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。

  1. 类型断言VS泛型

如下三段代码,分别是使用类型断言,类型声明和泛型对函数返回值类型的约束。可以看出,泛型 可以更加规范的实现对函数返回值的约束。泛型是最优的一个解决方案。

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();
function getCacheData(key: string): any {
    return (window as any).cache[key];
}

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

const tom: Cat = getCacheData('tom');
tom.run();
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();

三. 类型别名

类型别名用来给一个类型起个新名字,类型别名常用于联合类型。

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

四. 类型合并

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型:

函数的合并

我们可以使用重载定义多个函数类型

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('');
    }
}

接口的合并

接口中的属性在合并时会简单的合并到一个接口中:

interface Alarm {
    price: number;
}
interface Alarm {
    weight: number;
}

相当于

interface Alarm {
    price: number;
    weight: number;
}

注意,合并的属性的类型必须是唯一的

interface Alarm {
    price: number;
}
interface Alarm {
    price: string;  // 类型不一致,会报错
    weight: number;
}

类的合并

类的合并与接口的合并规则一致。