什么是TypeScript?
TypeScript 是添加了类型系统的 JavaScript,适用于任何规模的项目。
TypeScript 是一门静态类型、弱类型的语言。
安装TypeScript
npm install -g typescript
编译
tsc hello.ts
TypeScript 只会在编译时对类型进行静态检查,如果发现有错误,编译的时候就会报错。而在运行时,与普通的 JavaScript 文件一样,不会对类型进行检查。
TypeScript 编译的时候即使报错了,还是会生成编译结果。
基础
1、原始数据类型
JavaScript基本类型 | TypeScript基本类型 | 备注 |
---|---|---|
布尔值 | boolean | 注意Boolean对象不是布尔值 |
数值 | number | ES6中的二进制和八进制会被编译为十进制数字 |
字符串 | string | |
空值 | void | Js中没有空值的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数 |
Null | null | |
Undefined | undefined | undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量,而void 类型的变量不能赋值给 number 类型的变量 |
2、任意值
- 任意值(Any)用来表示允许赋值为任意类型。,普通类型在赋值过程中改变类型是不被允许的;
- 在任意值上访问任何属性都是允许的;
- 在任意值上调用任何方法都是允许的;
- 声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。
- 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型
3、类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
举个例子:
let a = '123'
a = 1
编译
tsc hello.ts
hello.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'.
2 a = 1
~
Found 1 error in hello.ts:2
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查。
4、联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
联合类型使用 | 分隔每个类型。
let a: string | number
a = 'app'
a = 1
当一个变量被声明(但未赋值时)为联合类型,仅能访问联合类型中所有类型都共有的属性或方法。
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类,此时可以正常访问该类型的属性或方法。
5、对象的类型——接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
// 定义接口
interface Animal {
type: string,
age: number
}
// 使用该接口声明变量
let cat : Animal = {
name: 'mimi',
age: 2
}
可选属性
在使用接口声明变量时,多声明属性或少声明属性都不被允许。
但我们可以使用可选属性来达到不完全匹配接口属性的目的,可选属性的含义是该属性可以不存在,但仍然不允许添加未定义的属性。
// 定义接口
interface Animal {
type: string,
age?: number
}
任意属性
在接口中定义任意属性,即可让我们能够在声明变量是添加一个任意的属性。
// 定义接口
interface Animal {
type: string,
[propName: string]: any;
}
// 声明变量
// 使用该接口声明变量
let cat : Animal = {
name: 'mimi',
gender: 'female'
}
注意:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:
interface Animal {
type: string,
age: number
[propName: string]: string;
}
上面这段代码会报错,因为任意属性只允许string,但age属性声明为了number
只读属性
使用readonly定义只读属性,该属性仅在创建的时候可被赋值
注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
6、数组的类型
在TypeScript中可以使用多种方式定义数组:
- 类型+方括号:
let arr: number[] = [1,2,3,4,5]
- 数组泛型:
let arr: Array<number> = [1,2,3,4,5]
- 用接口便是数组:
interface NumberArray {
[index: number]: number
}
let arr:NumberArray = [1,2,3,4,5]
// NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。
- 类数组:
类数组不是数组类型,比如函数的arguments
参数
function sum() {
let args: number[] = arguments;
}
// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
上例中,arguments 实际上是一个类数组(常用的类数组有:IArguments
,NodeList
, HTMLCollection
),不能用普通的数组的方式来描述,而应该用接口:
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
另外,可以使用any表示数组中允许出现任意类型:
let arr: any[] = ['a', 1, true]
7、函数的类型
js中有两种常见的函数定义方式:
- 函数声明
- 函数表达式
函数声明
输入多余的(或者少于要求的)参数,是不被允许的
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 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
用接口定义函数的形状
interface SearchFunc {
(source: stirng, 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');
参数默认值
TypeScript 会将添加了默认值的参数识别为可选参数:
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat'); // Tom Cat
let cat = buildName(undefined, 'Cat'); // Tom Cat
剩余参数
重载
// 函数定义
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('');
}
}
注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
8、类型断言
语法
值 as 类型
// 例如 mimi as Cat
用途
将一个联合类型断言为其中其中一个类型
我们知道,我们只能访问联合类型中所有类型公有的属性和方法,否则在编译时会报错,为了“欺骗”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;
}
但这种方法只能避免编译错误,无法避免运行时错误。
使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
将一个父类断言为更具体的子类
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;
}
将任何一个类型断言为any
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
此时可以使用断言,将window断言为any类型
(window as any).foo = 1
将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any。
将any断言为一个具体的类型
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 能够被断言为 B,B 也能被断言为 A
9、声明文件
declare var
声明全局变量
declare function
声明全局方法
declare class
声明全局类
declare enum
声明全局枚举类型
declare namespace
声明(含有子属性的)全局对象
interface
和 type
声明全局类型
export
导出变量
export namespace
导出(含有子属性的)对象
export default ES6
默认导出
export = commonjs
导出模块
declare global
扩展全局变量
declare module
扩展模块
/// <reference />
三斜线指令
声明语句
declare var jQuery: (selector: string) => any;
jQuery('#foo');
声明文件
声明文件即:将声明语句放在一个单独的文件(jQuery.d.ts)中
声明文件必需以 .d.ts
为后缀。
如何书写声明文件?
declare var /declare let/ declare const
declare const name = 'abc'
// 声明语句中只能定义类型,切勿在声明语句中定义具体的实现,以下代码会报错
declare const getAge = function(name: string) {
return 18
}
declare function
declare function jQuery(selector: string): any;
declare class
也只能定义类型,不能写具体实现
declare class {
name: string,
age: number,
sayHi(): string
}
declare enum
定义外部枚举
声明文件:
// 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 是由第三方库定义好的全局变量。
npm包
如何看npm包是否存在声明文件?
npm包的声明文件可能存在于2个地方:
- 与该 npm 包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。
- 发布到 @types 里。我们只需要尝试安装一下对应的 @types 包就知道是否存在该声明文件,安装命令是 npm install @types/foo --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。
如果通过以上两种方式没有找到对应的声明文件,则需要自己编写:
创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 paths 和 baseUrl 字段。
如何编写可参考:http://ts.xcatliu.com/basics/declaration-files.html
模块插件 declare module
有时通过 import 导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 declare module,它可以用来扩展原有模块的类型。
扩展原有模块:
// types/moment-plugins/index.d.ts
import * as moment from 'moment' // 引入原模块
declare module 'moment' {
export function foo(): moment.Calendarkey
}
// src/index.ts
import * as moment from 'moment';
import 'moment-plugin';
moment.foo();
声明文件中的依赖
一个声明文件有时会依赖另一个声明文件中的类型,比如在前面的 declare module 的例子中,我们就在声明文件中导入了 moment,并且使用了 moment.CalendarKey 这个类型:
// types/moment-plugin/index.d.ts
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}
除了可以在声明文件中通过 import 导入另一个声明文件中的类型之外,还有一个语法也可以用来导入另一个声明文件,那就是三斜线指令。
三斜线指令用于声明模块之间的依赖关系
类似于声明文件中的 import,它可以用来导入另一个声明文件。与 import 的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import:
- 当我们在书写一个全局变量的声明文件时
- 当我们需要依赖一个全局变量的声明文件时
1、书写一个全局变量的声明文件
在全局变量的声明文件中,是不允许出现 import, export 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了
三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释
// env.d.ts
/// <reference types="vite/client" />
2、依赖一个全局变量的声明文件
在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当然也就必须使用三斜线指令来引入了
// types/node-plugin/index.d.ts
/// <reference types="node" />
export function foo(p: NodeJS.Process): string;
// src/index.ts
import { foo } from 'node-plugin';
foo(global.process);
在上面的例子中,我们通过三斜线指引入了 node 的类型,然后在声明文件中使用了 NodeJS.Process 这个类型。最后在使用到 foo 的时候,传入了 node 中的全局变量 process。
由于引入的 node 中的类型都是全局变量的类型,它们是没有办法通过 import 来导入的,所以这种场景下也只能通过三斜线指令来引入了。