欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

TypeScript基础学习记录

程序员文章站 2022-06-22 16:40:31
开坑typescript,记录基础部分的知识点参考教程:https://github.com/xcatliu/typescript-tutorial文章目录1、数据类型简单的数据单元:复杂一点的:任意值null和undefined2、类型推论3、联合类型4、接口——Interfaces5、数组的类型类数组any 在数组中的应用6、函数的类型函数声明函数表达式用接口定义函数的形状函数的可选参数参数默认值剩余参数重载7、类型断言语法用途1、将一个联合类型断言为其中一个类型2、将一个父类断言为更加具体的子....

开坑typescript,记录基础部分的知识点
参考教程:https://github.com/xcatliu/typescript-tutorial

1、数据类型

简单的数据单元:

boolean、number、string

复杂一点的:

Array、Tuple、enum

数组Array两种定义方式:

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

元组Tuple:

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

let x: [string, number];
x = ['hello', 10];
// 访问它
x[0].substr(1) // √
x[1].substr(1) // ×,number类型没有substr

越界访问的时候,会使用联合类型替代:

x[3] = 'world'; // √, 字符串可以赋值给(string | number)类型
x[5].toString() // √, 'string' 和 'number' 都有 toString方法
x[6] = true; // ×, 布尔不是(string | number)类型

枚举enum:

enum类型是对JavaScript标准数据类型的一个补充。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认从0开始为元素编号,也可以手动指定:

enum Color {Red = 1, Green, Blue} // 从1开始
enum Color {Red = 1, Green = 2, Blue = 4} // 编号1 2 4

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,想知道数值为2的color名:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName);  // 'Green'

任意值

任意值any用于定义那些还不清楚类型的变量,将它们定义为any,是希望类型检查器可以直接让它们通过编译阶段的检查:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

null和undefined

在 TypeScript 中,可以用 void 表示没有任何返回值的函数。空值(Void),只能将它赋值为 undefinednull

null和undefined,与void的区别是:undefinednull 是所有类型的子类型。

也就是说 undefined 类型的变量,可以赋值给 number 类型的变量,而 void 类型的变量不能赋值给 number 类型的变量。

2、类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。也就是说TypeScript会在没有明确指定类型的时候推测出一个类型,比如说字符串:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7; // 报错,不能把number值赋给string

假如不赋值,那会被推断为any类型,也就不受类型检查:

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

3、联合类型

类似于any,可以让取值为多种类型中的一种:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // √
myFavoriteNumber = 7; // √ 

至于联合类型的属性和方法,共有的那些才能访问,比如说toString();如果是非共有的,比如length,是字符串或者数组才有的,联合类型中包含number就不行了。

function getLength(something: string | number): number {
    return something.length;
}
// Property 'length' does not exist on type 'string | number'.

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // 判断是字符串
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7; // 判断是数字
console.log(myFavoriteNumber.length); // 报错,现在是number类型

4、接口——Interfaces

TypeScript 中的接口是一个非常灵活的概念,个人感觉是可以是JavaScript中的(接口其实就是对象类型的规范啦)。

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

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

接口名一般首字母大写,定义的变量不能缺少属性,比如说上面的tom,不能只有name,否则报错。

多一些属性也是不允许的,言下之意就是,赋值的变量,形状要和接口定义的一致。所以说TypeScript 中的接口也可以说是用来描述对象的形状

有时我们希望形状并不完全一致,所以可以设置可选属性(?:):

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

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。像上面的any,显然上string、number的超集。

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型。

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

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

还有的时候我们希望属性在创建后不被更改(类似const),就可以用只读属性 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;

需要注意的是,只读属性的只读约束是说第一次为对象赋值的时候,字段就已经确定了值。假如之前只读属性在对象创建的过程中没有被赋值,那么第一次给只读属性赋值的操作将会是失败的。

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

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

tom.id = 89757; // 报错

tom.id在创建时没有赋值,所以在第一次给id属性赋值的时候就会报错。

5、数组的类型

TypeScript中,数组类型有多种表示方法:

第一种:【类型+方括号】表示法

let fibonacci: number[] = [1, 1, 2, 3, 5];

第二种:数组泛型 Array<elemType>

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

第三种:接口表示数组:

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

虽然接口也可以用来描述数组,但是一般不会这么做,因为这种方式比前两种方式复杂多了。

类数组

不过有一种情况例外,那就是它常用来表示类数组。

类数组(Array-like Object)不是数组类型,比如 arguments

function sum() {
    let args: number[] = arguments;
}

arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口:

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

常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

function sum() {
    let args: IArguments = arguments;
}

其中 IArguments 是 TypeScript 中定义好了的类型,它实际上就是:

interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

any 在数组中的应用

any 表示数组中允许出现任意类型:

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

6、函数的类型

函数声明

需要把输入和输出都考虑到:

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

注意,输入多余的(或者少于要求的)参数,是不被允许的

函数表达式

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

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。在 ES6 中,=> 叫做箭头函数,应用十分广泛。

用接口定义函数的形状

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

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

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

函数的可选参数

与接口中的可选属性类似,用 ? 表示可选的参数:

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

注意,可选参数后不允许继续出现必需参数。

参数默认值

ES6中可以给函数参数设置默认值,而TypeScript则会将添加了默认值的参数,识别为可选参数。

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

此时就不受「可选参数必须接在必需参数后面」的限制了,默认值可以在必需参数之前设置。

剩余参数

ES6中使用 ...rest 的方式获取函数中的剩余参数(rest 参数),在TypeScript中,可以用一个any类型的数组去定义它:

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

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

注意,rest 参数只能是最后一个参数

重载

允许一个函数接受不同数量或者类型的参数,并作出不同的处理。这在ES6中通过剩余参数或者参数类型判断是可以做到的。那TypeScript的话可以通过联合类型等方式去实现:

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

上面的代码中,根据x的类型,选择了不同的表达式作为结果。这样有一个缺点,就是不能精确表达,输入和输出的类型应当一致。

因此,我们可以使用重载定义(有C++内味了)多个函数类型:

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进行了重复的定义,最后一次是函数的具体实现。编辑器中可以看到前两个函数是被overload的。

TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

7、类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

语法

as 类型

<类型>

在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用 值 as 类型;形如 <Foo> 的语法在 tsx 中表示的是一个 ReactNode,在 ts 中除了表示类型断言之外,也可能是表示一个泛型

故建议在使用类型断言时,统一使用 值 as 类型 这样的语法。

用途

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

在 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问联合类型中的共有属性和方法。如果希望避开报错提示,就可以使用类型断言 值 as 类型

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

需要注意,类型断言只能够「欺骗」TypeScript 编译器,但无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:

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

function swim(animal: Cat | Fish) {
    (animal as Fish).swim();
}

const tom: Cat = {
    name: 'Tom',
    run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function

编译时TypeScript可以通过断言,但在运行时,swim(tom)传入了一个Cat类型的变量,由于它不具有swim()方法,接着就会跳出错误。故使用类型断言要慎之又慎。

2、将一个父类断言为更加具体的子类(向下兼容?)

我们想实现一个函数,来判断传入变量是否是子类,js中这样实现:

class ApiError extends Error {
    code: number = 0;
}
class HttpError extends Error {
    statusCode: number = 200;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

在isApiError中,error的类型是父类。由于父类Error没有code属性,最后是会报错的。我们可以使用instanceof去判断error的具体类型,但问题是接口类型并不是真正的值,在经过编译之后会被删除,所以运行时是无法做判断的:

interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (error instanceof ApiError) {
        return true;
    }
    return false;
}
// error TS2693: 'ApiError' only refers to a type, but is being used as a value here.

此时只能通过类型断言,来判断是否存在code属性,进而判断传入参数的类型是否是ApiError,TypeScript中这样实现:

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;
}
3、将任何一个类型断言为any

有时候为了访问变量的所有属性(或者添加属性),我们需要把变量类型断言为any,比如说window:

window.foo = 1;
// error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
(window as any).foo = 1;

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

需要注意的是,将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段

4、将any断言为一个具体的类型

遇到 any 类型的变量时,我们可以选择无视它,任由它滋生更多的 any

我们也可以选择改进它,通过类型断言及时的把 any 断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。

举例来说,历史遗留的代码中有个 getCacheData,它的返回值是 any

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

那么我们在使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作:

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

上面的例子中,我们调用完 getCacheData 之后,立即将它断言为 Cat 类型。这样的话明确了 tom 的类型,后续对 tom 的访问时就有了代码补全,提高了代码的可维护性。

类型断言的限制

类型断言有没有什么限制呢?是不是任何一个类型都可以被断言为任何另一个类型呢?

答案是否定的——并不是任何一个类型都可以被断言为任何另一个类型。

具体来说,若 A 兼容 B,那么 A 能够被断言为 BB 也能被断言为 A

综上所述:

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

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

双重断言

双重断言 as any as Foo 来将任何一个类型断言为任何另一个类型,它很可能会导致运行时错误。

除非迫不得已,千万别用双重断言。

类型断言 vs 类型转换

类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:

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

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

在上面的例子中,将 something 断言为 boolean 虽然可以通过编译,但是并没有什么用,代码在编译后会变成:

function toBoolean(something) {
    return something;
}

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

所以类型断言不是类型转换,它不会真的影响到变量的类型。

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

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

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

类型断言 vs 类型声明

类型声明是比类型断言更加严格,断言需要A类兼容B类或者B类兼容A类;而声明则是要求右边的运算内容(子类)兼容左边的变量声明类型。

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

类型断言 vs 泛型

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

getCacheData 函数添加了一个泛型 <T>,我们可以更加规范的实现对 getCacheData 返回值的约束,这也同时去除掉了代码中的 any,是最优的一个解决方案。

8、声明文件(这一节比较冗长,可以跳过)

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

声明语句

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $jQuery 了。

但是在 ts 中,编译器并不知道 $jQuery 是什么东西:

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

这时,我们需要使用 declare var 来定义它的类型:

declare var jQuery: (selector: string) => any;
jQuery('#foo');

上例中,declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

jQuery('#foo');

声明文件

通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件:

// src/jQuery.d.ts

declare var jQuery: (selector: string) => any;
// src/index.ts

jQuery('#foo');

声明文件必需以 .d.ts 为后缀。

一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 jQuery.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得 jQuery 的类型定义了。

/project
├── src
|  ├── index.ts
|  └── jQuery.d.ts
└── tsconfig.json

假如仍然无法解析,那么可以检查下 tsconfig.json 中的 filesincludeexclude 配置,确保其包含了 jQuery.d.ts 文件。

PS:插点关于 tsconfig.json的知识(tsconfig.json概述

我在编写的时候,tsconfig.json的内容基本没什么改动,files、include还有exclude没有配置,但能读取到jQuery.d.ts,vscode没有报错。这是因为,如果"files""include"都没有被指定,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts, .d.ts.tsx),排除在"exclude"里指定的文件。

第三方声明文件

社区已经帮我们定义好jQuery 的声明文件:jQuery in DefinitelyTyped

我们可以直接下载下来使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。

@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

npm install @types/jquery --save-dev

可以在这个页面搜索你需要的声明文件。

书写声明文件

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事,以下会详细介绍如何书写声明文件。

在不同的场景下,声明文件的内容和使用方式会有所区别。

使用场景主要有以下几种:

  • 全局变量:通过 <script> 标签引入第三方库,注入全局变量
  • npm 包:通过 import foo from 'foo' 导入,符合 ES6 模块规范
  • UMD 库:既可以通过 <script> 标签引入,又可以通过 import 导入
  • 直接扩展全局变量:通过 <script> 标签引入后,改变一个全局变量的结构
  • 在 npm 包或 UMD 库中扩展全局变量:引用 npm 包或 UMD 库后,改变一个全局变量的结构
  • 模块插件:通过 <script>import 导入后,改变另一个模块的结构
全局变量

全局变量的声明文件主要有以下几种语法:

  • declare var 声明全局变量
  • declare function声明全局方法
  • declare class声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
declare var

在所有的声明语句中,declare var 是最简单的,如之前所学,它能够用来定义一个全局变量的类型。与其类似的,还有 declare letdeclare const,使用 let 与使用 var 没有什么区别:

// src/jQuery.d.ts
declare let jQuery: (selector: string) => any;
// src/index.ts
jQuery('#foo');
// 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
jQuery = function(selector) {
    return document.querySelector(selector);
};

一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const 而不是 varlet

需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现:

declare const jQuery = function(selector) {
    return document.querySelector(selector);
};
// ERROR: An implementation cannot be declared in ambient contexts.
declare function

declare function 用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 function 来定义:

// src/jQuery.d.ts
declare function jQuery(selector: string): any;

在函数类型的声明语句中,函数重载也是支持的:

// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
// src/index.ts
jQuery('#foo');// 字符串类型
jQuery(function() {
    alert('Dom Ready!');
});// 函数类型
declare class

declare class 用于定义一个类的全局变量,同样的,只能够定义类型,而不能定义具体的实现(类的方法):

// src/Animal.d.ts

declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string; // 正确
    sayHi() {
        return `My name is ${this.name}`;
    };
    // ERROR: An implementation cannot be declared in ambient contexts.
}
declare enum

使用 declare enum 定义的枚举类型也称作外部枚举(Ambient Enums),举例如下:

// src/Directions.d.ts
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
// src/index.ts
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

同样的,只能定义类型,而不是具体的值,在编译完成后,Directions.d.ts文件内的内容会被删除,编译结果只剩下index.ts。

declare namespace

namespace 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。

由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module 关键字表示内部模块。但由于后来 ES6 也使用了 module 关键字,ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间(老命名冲突了)。

随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 namespace 的使用了。

namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性

比如 jQuery 是一个全局变量,它是一个对象,提供了一个 jQuery.ajax 方法可以调用,那么我们就应该使用 declare namespace jQuery 来声明这个拥有多个子属性的全局变量。

// src/jQuery.d.ts
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    const version: number;
    class Event {
        blur(eventType: EventType): void
    }
    enum EventType {
        CustomClick
    }
}

// src/index.ts
jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);

如果对象拥有深层的层级,则需要用嵌套的 namespace 来声明深层的属性的类型

declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    namespace fn {
        function extend(object: any): void;
    }
}
interfacetype

除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 interfacetype 来声明一个全局的接口或类型

// src/jQuery.d.ts
interface AjaxSettings {
    method?: 'GET' | 'POST'
    data?: any;
}
declare namespace jQuery {
    function ajax(url: string, settings?: AjaxSettings): void;
}

在其他的文件中,也可以继续使用这个接口或者类型。type同理。

防止命名冲突

暴露在最外层的 interfacetype 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 namespace 下。并在使用它们时,加上jQuery前缀。

声明合并

假设jQuery是一个函数,又是一个对象(不用假设,确实是这样用的),那么可以组合多个声明语句,它们将不会冲突:

// src/jQuery.d.ts

declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}
// src/index.ts

jQuery('#foo');
jQuery.ajax('/api/get_something');
npm包

npm 包的声明文件主要有以下几种语法:

  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
export

npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export 导出,然后在使用方 import 导入后,才会应用到这些类型声明。

// types/foo/index.d.ts

export const name: string;
export function getName(): string;
export class Animal {
    constructor(name: string);
    sayHi(): string;
}
export enum Directions {
    Up,
    Down,
    Left,
    Right
}
export interface Options {
    data: any;
}

混用declare和export:

// types/foo/index.d.ts

declare const name: string;
declare function getName(): string;
declare class Animal {
    constructor(name: string);
    sayHi(): string;
}
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
interface Options { // interface 之前是不需要用declare的
    data: any;
}

export { name, getName, Animal, Directions, Options };
export namespace

declare namespace 类似,export namespace 用来导出一个拥有子属性的对象:

// types/foo/index.d.ts

export namespace foo {
    const name: string;
    namespace bar {
        function baz(): string;
    }
}
// src/index.ts

import { foo } from 'foo';

console.log(foo.name);
foo.bar.baz();
export default

在 ES6 模块系统中,使用 export default 可以导出一个默认值,使用方可以用 import foo from 'foo' 而不是 import { foo } from 'foo' 来导入这个默认值。

在类型声明文件中,export default 用来导出默认值的类型:

// types/foo/index.d.ts
export default function foo(): string;

// src/index.ts
import foo from 'foo';

foo();

注意,只有 functionclassinterface 可以直接默认导出,其他的变量需要先定义出来,再默认导出:

// types/foo/index.d.ts

export default enum Directions {
// ERROR: Expression expected.
    Up,
    Down,
    Left,
    Right
}

// 正确的做法,先定义,再导出
// 同时,针对这种默认导出,我们建议把导出语句放在整个声明文件的最前面
// types/foo/index.d.ts
export default Directions;

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
export =

在 commonjs 规范中,我们用以下方式来导出一个模块:

// 整体导出
module.exports = foo;
// 单个导出
exports.bar = bar;

在 ts 中,针对这种模块导出,有多种方式可以导入,第一种方式是 const ... = require

// 整体导入
const foo = require('foo');
// 单个导入
const bar = require('foo').bar;

第二种方式是 import ... from,注意针对整体导出,需要使用 import * as 来导入:

// 整体导入
import * as foo from 'foo';
// 单个导入
import { bar } from 'foo';

第三种方式是 import ... require,这也是 ts 官方推荐的方式:

// 整体导入
import foo = require('foo');
// 单个导入
import bar = foo.bar;

对于这种使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到 export = 这种语法了:

// types/foo/index.d.ts

export = foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}

注意,上例中使用了 export = 之后,就不能再单个导出 export { bar } 了。所以我们通过声明合并,使用 declare namespace foo 来将 bar 合并到 foo 里。

实际上,import ... requireexport = 都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,并不常用也不推荐使用。

由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到 export = 这种语法了。但是还是需要再强调下,相比与 export =,我们更推荐使用 ES6 标准的 export defaultexport

UMD 库

既可以通过 <script> 标签引入,又可以通过 import 导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 export as namespace

export as namespace

一般使用 export as namespace 时,都是先有了 npm 包的声明文件,再基于它添加一条 export as namespace 语句,即可将声明好的一个变量声明为全局变量,举例如下:

// types/foo/index.d.ts

export as namespace foo;
export = foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}

当然它也可以与 export default 一起使用:

// types/foo/index.d.ts

export as namespace foo;
export default foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}
直接扩展全局变量

有的第三方库扩展了一个全局变量,可是此全局变量的类型却没有相应的更新过来,就会导致 ts 编译错误,此时就需要扩展全局变量的类型。比如扩展 String 类型

interface String {
    prependHello(): string;
}

'foo'.prependHello();

通过声明合并,使用 interface String 即可给 String 添加属性或方法。

在 npm 包或 UMD 库中扩展全局变量

如之前所说,对于一个 npm 包或者 UMD 库的声明文件,只有 export 导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global

declare global

使用 declare global 可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型:

// types/foo/index.d.ts
declare global {
    interface String {
        prependHello(): string;
    }
}

export {};

// src/index.ts
'bar'.prependHello();

注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。

模块插件

有时通过 import 导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 declare module,它可以用来扩展原有模块的类型。

declare module
// types/moment-plugin/index.d.ts
import * as moment from 'moment';
// 先引用原有模块,再用 declare module 扩展原有模块
declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

// src/index.ts
import * as moment from 'moment';
import 'moment-plugin';

moment.foo();

declare module 也可用于在一个文件中一次性声明多个模块的类型。

声明文件中的依赖

一个声明文件有时会依赖另一个声明文件中的类型,除了可以用import来引入,还可以用三斜线指令的语法(你可以在jQuery的声明文件中看到类似的实现):

// types/jquery-plugin/index.d.ts

/// <reference types="jquery" />

declare function foo(options: JQuery.AjaxSettings): string;

三斜线指令的语法如上,/// 后面使用 xml 的格式添加了对 jquery 类型的依赖,这样就可以在声明文件中使用 JQuery.AjaxSettings 类型了。

注意,三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。

拆分声明文件
// node_modules/@types/jquery/index.d.ts

/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />

export = jQuery;

这里用到了typespath 两种不同的指令。它们的区别是,types 用于声明对另一个库的依赖,而path用于声明对另一个文件的依赖。

自动生成声明文件

如果库的源码本身就是由 ts 写的,那么在使用 tsc 脚本将 ts 编译为 js 的时候,添加 declaration 选项,就可以同时也生成 .d.ts 声明文件了。

我们可以在命令行中添加 --declaration(简写 -d),或者在 tsconfig.json 中添加 declaration 选项。这里以 tsconfig.json 为例:

{
    "compilerOptions": {
        "module": "commonjs",
        "outDir": "lib",
        "declaration": true,
    }
}

上例中我们添加了 outDir 选项,将 ts 文件的编译结果输出到 lib 目录下,然后添加了 declaration 选项,设置为 true,表示将会由 ts 文件自动生成 .d.ts 声明文件,也会输出到 lib 目录下。

9、内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。

内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。

ECMAScript 的内置对象

ECMAScript 标准提供的内置对象有:

BooleanErrorDateRegExp 等。

我们可以在 TypeScript 中将变量定义为这些类型:

let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

DOM 和 BOM 的内置对象

DOM 和 BOM 提供的内置对象有:

DocumentHTMLElementEventNodeList 等。

TypeScript 中会经常用到这些类型:

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});

它们的定义文件同样在 TypeScript 核心库的定义文件中。

TypeScript 核心库的定义文件

TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。

当你在使用一些常用的方法的时候,TypeScript 实际上已经帮你做了很多类型判断的工作了,比如:

Math.pow(10, '2');

// index.ts(1,14): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

上面的例子中,Math.pow 必须接受两个 number 类型的参数。事实上 Math.pow 的类型定义如下:

interface Math {
    /**
     * Returns the value of a base expression taken to a specified power.
     * @param x The base value of the expression.
     * @param y The exponent value of the expression.
     */
    pow(x: number, y: number): number;
}

再举一个 DOM 中的例子:

document.addEventListener('click', function(e) {
    console.log(e.targetCurrent);
});

// index.ts(2,17): error TS2339: Property 'targetCurrent' does not exist on type 'MouseEvent'.

上面的例子中,addEventListener 方法是在 TypeScript 核心库中定义的:

interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {
    addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
}

所以 e 被推断成了 MouseEvent,而 MouseEvent 是没有 targetCurrent 属性的,所以报错了。

注意,TypeScript 核心库的定义中不包含 Node.js 部分。

用 TypeScript 写 Node.js

Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:

npm install @types/node --save-dev

谢谢阅读~

本文地址:https://blog.csdn.net/qq_40340478/article/details/107386864

相关标签: TypeScript学习