本部分主要是一些基础概念,掌握后可以从JavaScript语法习惯转变为TypeScript。
基础类型
表示形式:
1 | const foo:string = "test" |
- 字符串:
string
- 数字:
number
- 布尔值:
boolean
- 空值:
void
(主要用于表示函数无返回值或返回值为null时的定义) - 任意值:
any
(意味着不限制类型,可以为任意值) - 其他:
undefined
、null
类型推断
TS提供两种类型推论机制(代码中未定义类型时,编译时自动增加类型)
1 | let bar // 等同于 let bar:any |
需要注意的是,在使用类型推断后,不能在随意修改为其他类型,例如
1 | let bar = "string" // 此时相当于被ts推断为了 let bar:string = "string" |
数组也是如此
1 | let bar = ['a','b'] |
对于对象
1 | let bar = { a: '123', b: 234, c: true } //自动被推断生成一个接口 |
联合类型
1 | let myFavoriteNumber: string | number; |
联合类型使用 |
分隔每个类型。
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,开发者只能访问此联合类型的所有类型里共有的属性或方法
1 | function getLength(something: string | number): number { |
1 | let myFavoriteNumber: string | number; |
交叉类型
extend是一种非常常见的模式,可以从多个对象合并为一个新的对象。而在TS中,交叉类型可以让你安全的使用此模式 ,通过&
对类型进行连接
1 | function extend<T extends object, U extends object,B>(first: T, second: U, dsf:B): T & U & B{ |
接口
接口用于规范对象的属性和方法,是对象的类型。
确定属性
1 | interface Person{ |
接口定义关键字interface
;每个字段定义完毕之后用分号分割
接口的定义与Java一致,实例赋值的时候不能比接口定义的少或者多,需要与接口的形状保持一致。
可选属性
使用?
修饰的属性是可选的,不要求实例必须实现该属性
1 | interface Person { |
这时仍然不允许添加未定义的属性
任意属性
在有些书上也会被称之为“索引签名”。 有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
1 | interface Person { |
使用[propName: string]定义了任意属性可以取any值
任意属性的string
是KeyType
,而any
代表ValueType
需要注意的是,任意属性被定义后,其他与任意属性相同KeyType
的各种属性在赋值时会先去校验一次任意属性的ValueType
以下代码将发生变异错误
1 | interface Person { |
上例中将任意属性改为联合类型即可
1 | [propName: string]: string | number; |
或者把任意属性的类型改成非string类型(当然这也就意味着新增的任意属性值只能为number了)
1 | [propName: number]: string; |
使用一组有限的字符串字面量作为属性
一个索引签名可以通过映射类型来使索引字符串为联合类型中的一员,如下所示:
1 | type Index = 'a' | 'b' | 'c'; |
只读属性
如果需要某个属性在对象赋值后就不能被改变可以在接口中使用readonly
关键字来定义只读属性
1 | interface Person { |
只读属性也是在定义时必必须要填的,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
数组类型
类型+方括号表示法
数组内容需要符合数组类型定义
1 | let fibonacci: number[] = [1, 1, 2, 3, 5]; //只能存在number类型的数组元素 |
数组泛型
也可以使用范型的方式来定义数组类型Array<elemType>
1 | let fibonacci: Array<number> = [1, 1, 2, 3, 5]; |
用接口表示数组
利用任意属性来表示数组
1 | interface NumberArray { |
通常不用这种方法来表示数组,但是这个方式可以有效的用来表示类数组
1 | function sum() { |
事实上常用的类数组都有自己的接口定义,如 IArguments
, NodeList
, HTMLCollection
[参考内置对象章节]等:
1 | function sum() { |
函数类型
函数声明
function
1 | function sum(a:number,b:number):number{ |
函数表达式
1 | //类型推断简写方式 |
箭头函数
1 | let sum = (a:number,b:number):number=>{ |
回调函数
1 | function map(array:any[],callback:(el?:any,index?:number,arr?:any[])=>any):any{ |
仿写一个TS版的Array.prototype.map方法。其中map的第二个形参就是回调函数定义的方法。
利用接口定义函数的形状
1 | interface sumFunc{ |
采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
可选参数
正常情况下,输入多余的(或者少于要求的)参数,是不允许的。但是可以用 ?
表示可选的参数
1 | function buildName(firstName: string, lastName?: string) { |
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了
函数重载
1 | function reverse(x: number): number; |
类型别名
可以为类型定义别名,定义后可以直接使用别名来进行变量声明
1 | type Name = string |
字面量类型
字面量类型用来约束取值只能是某几个字符串中的一个。
1 | type ab = 1 | '1' | true |
元组
数组是合并相同类型的对象,而元组可以合并不同类型的对象
定义一对值分别为 string
和 number
的元组:
1 | let tom: [string, number] = ['Tom', 25]; |
当赋值或访问一个已知索引的元素时,会得到正确的类型:
1 | let tom: [string, number]; |
也可以只赋值其中一项:
1 | let tom: [string, number]; |
但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。
1 | let tom: [string, number]; |
越界的元素
当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:
1 | let tom: [string, number]; |
注意ts本质上无法进行运行时检测,所以push被优化为了联合类型检测。但是,如果是直接字面量对数组进行赋值,编译是无法通过的。
1 | let tom:[string,number] |
枚举
普通枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
1 | enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; |
事实上,上面的例子会被编译为:
1 | var Days; |
手动赋值
1 | enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat} |
自动赋值规则:未指定值的key将会以前一个key的值加1作为默认值,如果前面没有key则从0开始
常数枚举
常数枚举是使用 const enum
定义的枚举类型:
1 | const enum Directions { |
常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。
上例的编译结果是:
1 | var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */]; |
外部枚举
外部枚举(Ambient Enums)是使用 declare enum
定义的枚举类型:
1 | declare enum Directions { |
之前提到过,declare
定义的类型只会用于编译时的检查,编译结果中会被删除。
上例的编译结果是:
1 | var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]; |
外部枚举与声明语句一样,常出现在声明文件中。
同时使用 declare
和 const
也是可以的:
1 | declare const enum Directions { |
编译结果:
1 | var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */]; |
断言
TS中的断言可以用于将当前值变成一个其他形状的兼容工具
语法
1 | 值 as 类型 // 推荐 |
或者
1 | <类型>值 |
声明文件
利用declare关键字声明的为全局模块可访问的实例
declare var
声明全局变量,类似的还有
declare let
和declare const
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
扩展模块///
三斜线指令
内置对象
TS提供了对内置对象的 类型声明定义。内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。
ES内置对象
提供了例如Boolean
、Error
、Date
、RegExp
等内置对象的声明
1 | let b:Boolean = new Boolean(1); |
DOM和BOM内置对象
DOM 和 BOM 提供的内置对象有:
Document
、HTMLElement
、Event
、NodeList
等。
TypeScript 中会经常用到这些类型:
1 | let body: HTMLElement = document.body; |
类
访问修饰符
修饰在属性和方法上,用于设置该属性和方法的访问权限
public: 在任何地方被访问到。
protected: 只能被类自身和子类访问,应用于构造函数上则意味着只能被子类继承
private: 只能被类自身访问,应用于构造函数上则意味着该类不能被继承
参数属性
修饰符和readonly
还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。
1 | class Animal { |
readonly 关键字
在属性前添加readonly
修饰意味着该属性为只读属性,如果修改该属性将会发生报错
抽象类
abstract
用于定义抽象类和其中的抽象方法。
抽象类不能被直接实例化,需要由子类继承实现 ,并且子类必须实现抽象类中的所有抽象方法。这一点几乎与JAVA对抽象类的设计思想一模一样。
1 | abstract class Animal{ |
类的类型
给类加上 TypeScript 的类型很简单,与接口类似:
1 | class Animal { |
流动的类型
复制类型和值
如果想要复制一个类,错误的办法是:
1 | class Foo {} |
正确的方式是利用import关键字进行引用,这里利用Hack方法演示下
1 | namespace importing { |
捕获变量的类型
在变量类型声明中利用typeof
来利用其他变量的类型,这允许你告诉编译器,一个变量的类型与其他类型相同。
1 | const foo = 123; |
捕获类成员的类型
和捕获变量类型一致,需要将类注解到一个声明变量上,通过这个声明变量就可以捕获成员类型咯
1 | class Foo{ |
捕获键的名称
keyof
操作符能让你捕获一个类型的键。例如,你可以使用它来捕获变量的键名称,在通过使用 typeof
来获取类型之后:
1 | const colors = { |
这允许你很容易地拥有像字符串枚举+常量这样的类型,如上例所示。
类与接口
接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。而事实上接口还有另外的用途
在TS中类与接口其实是比较容易混淆的盖点,接口是描述对象的形状,而类是描述对象的实际行为。在TS中,类和接口在一起还可以碰撞出更加灿烂的火光。
类继承接口
按照Java中的开发思想,一个类只能继承于一个父类。有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements
关键字来实现。这个特性大大提高了面向对象的灵活性。
在TypeScript中,类实现接口需要把接口中所有的方法都实现,否则会发生报错!
举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:
1 | interface Alarm { |
一个类可以实现多个接口:
1 | interface Alarm { |
上例中,Car
实现了 Alarm
和 Light
接口,既能报警,也能开关车灯。
接口继承接口
接口与接口之间可以是继承关系:
1 | interface Alarm { |
这很好理解,LightableAlarm
继承了 Alarm
,除了拥有 alert
方法之外,还拥有两个新方法 lightOn
和 lightOff
。
接口继承类
本质上声明一个类时,还会同时声明一个包含其实例属性和实例方法的同名类型,而该类型本质上和接口一致。因此与其说是“接口继承类”,不如说是“接口继承接口”
1 | class Point { |
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
例如:
1 | function createArray<T>(length: number, value: T): Array<T> { |
上例中,我们在函数名后添加了 <T>
,其中 T
用来指代任意输入的类型,在后面的输入 value: T
和输出 Array<T>
中即可使用了。
接着在调用的时候,可以指定它具体的类型为 string
。当然,也可以不手动指定,而让类型推论自动推算出来。
多个类型参数
定义泛型的时候,可以一次定义多个类型的参数:
1 | function swap<T, U>(tuple: [T, U]): [U, T] { |
泛型约束
可以通过extend
关键字对泛型的形状进行约束
1 | // 例如 |
多个泛型类型之间也可以进行约束
1 | function copyFields<T extends U , U>(target:T,source: U):T{ |
代码中,既然T收到了U的约束,那么T必须是U的超集。其本质上与受到接口的约束效果一致
泛型接口
之前接触过,可以使用接口的方式来定义一个函数学要符合的形状
1 | interface SearchFunc { |
而现在,可以使用泛型来定义接口
1 | interface ICreateArrayFun{ |
当然,泛型类型的定义也可以提到接口外面
1 | interface ICreateArrayFun<T>{ //将泛型提到此处 |
泛型类
与泛型函数类似,泛型也可以用于类的类型定义中
泛型参数的默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。(很少用到)
1 | function createArray<T = string>(length: number, value: T): Array<T> { |
泛型配合Axios使用
通常将把服务端返回的响应体通过interface来进行声明 interface.ts
1 | // 请求接口数据 |
将API单独抽离成模块时user.ts
1 | import axios from './axios'; // 此处的axios应当是已经处理后(添加通用配置,拦截器等)的axios实例对象 |
使用该接口
1 | import { fetchUserAccount } from './user'; |
声明合并
和函数重载一样,接口和类也可以重复声明,TS编译阶段会将他们合并到一块
1 | interface Person{ |
多个要合并的接口或者类之间可以有重复的属性,但是属性的类型必须是一致的。否则会发生报错
1 | interface Person{ |
而方法合并和函数重载规则一致
1 | interface Alarm { |
相当于:
1 | interface Alarm { |
TypeScript项目
编译上下文
所谓编译上下文指的是TS编译的范围,通过配置文件可以对当前编译上下文中的TS文件提供编译选项,而这个配置文件事实上指的就是tsconfig.json
tsconfig.json
开始使用 tsconfig.json
是一件比较容易的事,你仅仅需要写下:
1 | {} |
例如,在项目的根目录下创建一个空 JSON 文件。通过这种方式,TypeScript 将 会把此目录和子目录下的所有 .ts 文件作为编译上下文的一部分,它还会包含一部分默认的编译选项。
编译选项
可以通过compilerOptions
来定制当前编译上下文的编译选项
1 | { |
当配置完毕时,VScode将会提供对TS的即时编译,但是如果想从命令行运行TS编译器,可以通过以下方式:
- 直接运行
tsc
, 它会从当前目录或者父级目录寻找tsconfig.json
- 运行
tsc -p ./path-to-project-directory
,它会从指定路径进行编译
你也可以显式指定需要编译的文件:
1 | { |
你还可以使用 include
和 exclude
选项来指定需要包含的文件和排除的文件:
1 | { |
注意
使用
globs
:**/*
(一个示例用法:some/folder/**/*
)意味着匹配所有的文件夹和所有文件(扩展名为.ts/.tsx
,当开启了allowJs: true
选项时,扩展名可以是.js/.jsx
)。
声明空间
声明空间指的是TS在编译阶段会将类型和变量添加到编译声明空间中
声明空间可以分为变量声明空间和类型声明空间
类型声明空间
通过
interface
定义的接口都会被ts编译器添加到类型声明空间1
2
3
4export interface Person{ //将会提升到类型声明空间
name:string
age:number
}变量声明空间
类、变量、函数都会被ts编译器添加到变量声明空间中
1
2
3
4
5
6
7class Person{ //提升到变量声明空间 && 但是在声明类时如果类型声明空间中没有该类型,则会自动创建一个同名类型,其内容为类型中已声明的属性。
//如果类型声明空间中已存在该类型,则利用声明合并规则进行合并
constructor(public name:string){}
say(){
console.log(this.name)
}
}
全局声明空间的范围是当前编译上下文
默认声明的将会成为全局的声明空间,加入export后成为局部模块声明空间
`
文件模块
- Typescript
模块解析
就是指导 ts编译器
查找导入(import)内容的流程
- TS模块解析供有两种策略:
Classic
: 以前是TypeScript 默认的解析策略,目前仅用作向后兼容Node
: 与 NodeJS 模块机制一致的解析策略
Node模块解析策略
当导入路径不是相对路径时,模块解析将会模仿 Node 模块解析策略,下面我将给出一个简单例子:
当你使用
1
import * as foo from 'foo'
,将会按如下顺序查找模块:ts
./node_modules/foo
../node_modules/foo
../../node_modules/foo
- 直到系统的根目录
当你使用
1
import * as foo from 'something/foo'
,将会按照如下顺序查找内容
./node_modules/something/foo
../node_modules/something/foo
../../node_modules/something/foo
- 直到系统的根目录
当你使用
1
import xx from './foo'
将会按照如下顺序查找内容
优先查找.ts
后缀 如无则查找 .d.ts
同名文件
Classic解析策略
当你使用文件路径
1
import xx from './foo'
将会按照如下顺序查找内容
当你使用
1
import xx from 'foo'
将会按照如下顺序查找内容
重写文件查找策略
在TypeScript中,可以通过声明全局模块的方法,来绕过文件查找策略
1 | // global.d.ts |
接着
1 | // test.ts |
动态导入表达式
动态导入表达式是 ECMAScript 的一个新功能,它允许你在程序的任意位置异步加载一个模块,TC39 JavaScript 委员会有一个提案,目前处于第四阶段,它被称为 import() proposal for JavaScript。
此外,webpack bundler 有一个 Code Splitting
功能,它能允许你将代码拆分为许多块,这些块在将来可被异步下载。因此,你可以在程序中首先提供一个最小的程序启动包,并在将来异步加载其他模块。
这很自然就会让人想到(如果我们工作在 webpack dev 的工作流程中)TypeScript 2.4 dynamic import expressions 将会把你最终生成的 JavaScript 代码自动分割成很多块。但是这似乎并不容易实现,因为它依赖于我们正在使用的 tsconfig.json
配置文件。
webpack 实现代码分割的方式有两种:使用 import()
(首选,ECMAScript 的提案)和 require.ensure()
(最后考虑,webpack 具体实现)。因此,我们期望 TypeScript 的输出是保留 import()
语句,而不是将其转化为其他任何代码。
让我们来看一个例子,在这个例子中,我们演示了如何配置 webpack 和 TypeScript 2.4 +。
在下面的代码中,我希望懒加载 moment
库,同时我也希望使用代码分割的功能,这意味 moment
会被分割到一个单独的 JavaScript 文件,当它被使用时,会被异步加载。
1 | import(/* webpackChunkName: "momentjs" */ 'moment') |
这是 tsconfig.json
的配置文件:
1 | { |
重要的提示
- 使用
"module": "esnext"
选项:TypeScript 保留import()
语句,该语句用于 Webpack Code Splitting。- 进一步了解有关信息,推荐阅读这篇文章:Dynamic Import Expressions and webpack 2 Code Splitting integration with TypeScript 2.4.
从JavaScript迁移
一般来说,将 JavaScript 代码迁移至 TypeScript 包括以下步骤:
- 添加一个
tsconfig.json
文件; - 把文件扩展名从
.js
改成.ts
,开始使用any
来减少错误; - 开始在 TypeScript 中写代码,尽可能的减少
any
的使用; - 回到旧代码,开始添加类型注解,并修复已识别的错误;
- 为第三方 JavaScript 代码定义环境声明(*.d.ts)。