一份TypeScript的学习记录(持续更新)

200 阅读10分钟

之前在上家公司时候学习了一些ts的知识,很多也是边学边了解,但是一些复杂的用法上还是稍有欠缺,所以打算重新好好的从头学一遍,打个扎实的基础。本篇作为精简的学习知识记录,适合有一定基础及了解的人作为参考和复习。

基础篇

数据类型

布尔值
let isDone: boolean = true;

注意:使用构造函数Boolean创建的对象不是布尔值

let createdByNewBoolean: Boolean = new Boolean(1);

let createdByNewBoolean: boolean = new Boolean(1);
// 报错

直接调用 Boolean 也可以返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);

在 TypeScript 中,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。其他基本类型(除了 null 和 undefined)一样,不再赘述。

数字
let num: number = 1;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;
字符串
let name: string = 'tom';
Object

object表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API

declare function create(o: object | null): void; 

create({ prop: 0 }); // OK 
create(null); // OK 
create(42); // Error 
create("string"); // Error 
create(undefined); // Error
空值

表示没有任何返回的函数

function alertName(): void {
    alert('My name is Tom');
}

如果声明给又给变量则没有什么用,因为只能赋值给undefined 和 null

let unusable: void = undefined;
Null 和 Undefined
let u: undefined = undefined;
let n: null = null;

与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

// 这样不会报错
let num: number = undefined;

// 这样也不会报错
let u: undefined;
let num: number = u;

而 void 类型的变量不能赋值给 number 类型的变量:

let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
任意值

可以跳过类型检查,任何值都可以赋值给any类型。

let isDone: any = true;

在任意值上访问任何属性和调用任何方法都是允许的:

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);

anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
Never

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

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

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

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

关于anyunkownvodinever的区别,可以看这里TS中any、void、unknown、never之间的区别 - 掘金 (juejin.cn)

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

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

用接口来表示数组

interface NumberArray {
    [index: number]: number;
}
let list: NumberArray = [1, 1, 2, 3, 5];

类数组:类数组不能用普通数组方式来描述,而应该用接口

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}
元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。元组中规定的元素类型顺序必须是完全对照的。可以添加?来表示可选。

let tom: [string, number]; 
tom = ['hello', 10]; // OK 
tom = [10, 'hello']; // Error
tom = ['hello'];     // Error

元组越界:可以越界添加元素(不建议),但不可越界访问,同时需要提供所有元组类型中指定的项。(这里官方文档有一个错误,允许越届访问,但实际上会报错)

tom.push(12)  // OK
console(tom[2]) // Error  长度为 "2" 的元组类型 "[string, number]" 在索引 "2" 处没有元素。
枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景,枚举使用 enum 关键字来定义,枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射。

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

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

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

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

手动赋值

enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
// 未手动赋值的枚举项会接着上一个枚举项递增。
console.log(Days["Sun"] === 7); // true
console.log(Days["Tue"] === 2); // true

手动赋值重复:如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:

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

console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true

上面的例子中,递增到 3 的时候与前面的 Sun 的取值重复了,但是 TypeScript 并没有报错,导致 Days[3] 的值先是 "Sun",而后又被 "Wed" 覆盖了。

个人理解:类似对象,但是可以反向获取属性值,且以索引为默认值。

类型断言

可以用来手动指定一个值的类型,使用类型断言前提是,你清楚地知道一个实体具有比它现有类型更确切的类型。TypeScrip不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。TypeScript会假设你已经进行了必须的检查。

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

类型推论

如果没有明确指定类型,那么ts会依照现有值推断一个类型。

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// 报错 index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

它等价于

let myFavoriteNumber: string = 'seven';

联合类型

表示取值可以为多种类型中的一种。

let tom: string | number;
tom = 'name';  // OK
tom = 25;  // OK
tom = true  // Error

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

function getLength(something: string | number): number {
    return something.length;
    // Error
    // index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
    // Property 'length' does not exist on type 'number'.
    
    return something.toString();
    //OK
}

length不是stringnumber的共同属性所以报错。访问toString就没有问题。

联合类型的变量在赋值的时候,会根据类型推论的规则退u淡出一个类型

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

接口

在ts中接口是一个很重要的部分,接口的作用就是为类型命名和为你的代码或第三方代码定义契约。

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

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

赋值的时候,变量的形状必须和接口的形状保持一致,多和少属性都是不可行的。

属性可选

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

let tom: Person = {
    name: 'Tom'
};

任意属性

有时候我们希望一个接口允许有任意的属性,可以使用如下方式:

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

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

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

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

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

// index.ts(3,5):类型“number | undefined”的属性“age”不能赋给“string”索引类型“string”。

上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 numbernumber 不是 string 的子属性,所以报错了。

只读属性

interface Person {
    readonly id: number;
    name: string;
}

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

tom.id = 9527;
// 无法分配到 "id" ,因为它是只读属性。ts(2540).

泛型

泛型是非常重要的一个知识点。

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

有时候我们定义了一些函数,要考虑复用的可能,不仅支持当前的数据类型,也要支持未来使用时的其他类型,泛型的作用就在于此。

举个栗子

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

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

这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。

这时候,泛型就派上用场了:

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']
/ 可以通过类型推论
createArray(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 loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}
/ 类型“T”上不存在属性“length”.ts (2339)
/ 泛型 `T` 不一定包含属性 `length`,所以编译的时候报错了。

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

interface Lengthwise {
    length: number;
}

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

/ 但是如果传入的参数不包含length,就会报错
loggingIdentity(7);
/ 类型“number”的参数不能赋给类型“Lengthwise”的参数.ts (2345)

泛型接口

可以使用接口的方式来定义一个函数需要符合的形状

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

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

当然也可以使用含有泛型的接口来定义函数的形状

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

泛型类

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

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

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

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创建类型别名,就是给类型起个新的名字。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个。

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

handleEvent(document.getElementById('hello'), 'scroll');  // Ok
handleEvent(document.getElementById('world'), 'dblclick'); // Error,event 不能为 'dblclick'

传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class

TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。

篇幅较长,可以点击这里查看(暂时没写,占位)

类与接口

声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

  • declare var 声明全局变量
  • 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 /> 三斜线指令