初识 TypeScript
什么是 TS
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上
TS 的优点和缺点
优点:
- 增加了代码的可读性和可维护性。写代码的时候有更多的编译提示,代码语义更清晰易懂
- 包容性
- 拥有活跃的社区
缺点:
- 一定的学习成本;短期可能会增加一些开发成本,毕竟要多写一些类型的定义
- 集成到构建流程需要一些工作量
- 可能和一些库结合的不是很完美
详见 什么是 TS
基本使用方法
- 编译代码:在命令行用 tsc <文件名> 的方式编译 .ts 文件,
- 运行代码:然后用 node 执行生成的 js 文件
- 安装了 ts-node 之后可以在命令行直接 ts-node <文件名> 的方式编译并执行 .ts 文件
静态类型的深度理解
- 指定变量的类型
- 变量会具有这个类型的属性和方法
常用语法
基础类型
个人理解:对象类型就是非基础类型,包括 函数,自定义类型,类 等
基础类型中,布尔值,数字,字符串基本和 JS 一样;数组有两种定义方式:
1 | // 第一种(较常用) |
元组 Tuple
一种数组,表示一个已知元素数量和类型的数组,各元素的类型不必相同。eg:
1 | let list: [string, number] = ['Jacle', 23] |
当访问一个越界的元素,会使用联合类型替代 (联合类型:联合类型表示一个值可以是几种类型之一。用 | 产生)
1 | let list: [string, number] = ['Jacle', ’23’] |
枚举 enum
enum 类型是对 JavaScript 标准数据类型的一个补充。 像 C# 等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字
应用场景
普通 JS 的情况
1 | const Status = { // eg. 处理一些后端code会用到这种写法 |
应用枚举的情况
1 | enum Status { |
枚举成员的初始化
- 不对枚举成员的元素进行初始化的时候,默认其值是 0,1,2 … ;给某个成为初始化为某个数字都时候,后面的成员会默认接着这个数字进行初始化
1 | enum Status { |
- 对某个成员初始化为非数字类型时,接下来的其他成员都得进行显式初始化,直到某个成员被显示初始化为数字类型
1 | enum Status { |
更详见 官方文档:枚举
any (常用)
我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any 类型来标记这些变量
1 | let notSure: any = 4 |
void
void 类型像是与 any 类型相反,它表示没有任何类型。一般用于函数没返回值的时候使用;void 变量则只能赋值为 undefined 和 null
undefined 和 null (少用)
两者各自有自己的类型分别叫做 undefined 和 null。 和 void 相似,它们的本身的类型用处不是很大
never (少用)
never 类型表示的是那些永不存在的值的类型。 例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。eg:
1 | // 返回never的函数必须存在无法达到的终点 |
object
object 表示非原始类型,也就是除 number,string,boolean,symbol,null 或 undefined 之外的类型
1 | declare function create(o: object | null): void |
类型断言
使用类型断言的时候,TS 认为程序员已经很确定这个变量的类型了,会假设程序员已经做了必须的检查,常和 any
搭配使用。有两种写法:
1 | // <>写法 |
类型注解(type annotation) & 类型推断 (type inference)
类型注解:明显写出来的,我们告诉 TS 变量是什么类型
类型推断:TS 自动的去尝试分析变量类型。当 TS 无法分析出来的时候,就需要我们写类型注解
变量声明
同 JS:var,let, const
类型别名(type alias)
就是 type 这种语法
1 | type User = { |
个人理解:类型别名 和 接口 的区别不大,类型别名 能表示基础类型,接口 只能表示对象类型;能用接口的时候尽量用接口
接口
TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约
个人理解是:接口是对一类数据结构的约定和描述
可选属性
可有可无的属性。eg:color?: string;
只读属性
只能在刚创建的时候赋值,后面不能修改,在接口的属性前加 readonly
。eg:
1 | interface Point { |
此外,TypeScript 的数组具有 ReadonlyArray<T>
类型,效果一样
1 | let ro: ReadonlyArray<number> = [1, 2, 3] |
总结:不能变的属性用 readonly,不能变的变量用 const
一个作用:接口可能带有任意数量的其它属性
如果我们创建一个接口,该接口除了我们明确约定好的属性,还可能会带有任意数量的其它属性,我们这时候就可以用 添加一个字符串索引签名
1 | interface People { |
但是其实这种情况还有一个不用索引类型的知识点(虽然不太严谨):直接传的对象有多余属性会报错,传的是变量的时候就不会
1 | interface Person { |
函数类型
接口也可以描述函数类型
1 | interface searchFun { |
类类型
接口也能用来描述类,强制一个类去满足某种契约。用 implements
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员
1 | interface ClockInt { |
接口的继承
一个接口可以继承一个或多个接口,创建出多个接口的合成接口。用 extends
1 | interface class1 { |
可索引类型 && 索引签名
接口也可以描述那些能够“通过索引得到”的类型,比如数组或对象: a[10]
或 ageMap['daniel']
。 可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型(两部分)
一般情况下, 索引 和 索引签名 是一个概念;当我们声明一个索引签名的时候,就是声明了索引的类型,还有相应的索引返回值类型(两部分)
1 | interface StrArr { |
这个 index 只是发挥可读性的作用,你可以随便命名
索引签名
可看文档
JS中
- 在JS中,一般认为索引签名的类型是字符串和数字
1
2
3
4
5
6
7let jacle = {
name: 'jacle',
age: 17
}
console.log(jacle['name']) // 索引签名是字符串
let people = [jacle]
console.log(people[0]) // 索引签名是数字 - 当我们把索引签名搞成对象时,JavaScript 会在得到结果之前会先调用 .toString 方法:
1
2
3
4
5
6
7
8
9
10
11let obj = {
toString() {
console.log('toString called');
return 'Hello';
}
};
let foo: any = {};
foo[obj] = 'World'; // toString called
console.log(foo[obj]); // toString called, World
console.log(foo['Hello']); // World只要索引位置使用了 obj,toString 方法都将会被调用
TS中
TypeScript 的索引签名必须是 string 或者 number。我们搞 obj 进去会报错的,强制用户必须明确的写出 toString()
声明一个索引签名
1 | const foo: { |
所有成员都必须符合字符串的索引签名
1 | // ok |
使用字符串字面量(字符串组成的联合类型)当索引签名
一个索引签名可以通过映射类型来使索引字符串为联合类型中的一员:
映射类型是 in、keyof、typeof 这些?
1 | type index = 'a' | 'b' | 'c'; |
同时拥有 string 和 number 类型的索引签名(少用)
TypeScript 可以同时使用字符串和数字两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型
1 | interface ArrStr { |
类
基本例子
1 | class Greeter { |
创建了一个 Greeter 类,有一个 greeting 属性,一个构造函数 constructor(所以构造函数中也得有 this.greeting)和一个 greet 方法
继承
同 ES6
此外,子类可以通过直接重写方法的形式重写父类的方法;重写的时候如果想调用父类的方法可以用 super
1 | class Person { |
公共,私有与受保护的修饰符
类的属性,静态属性,方法,构造函数 都可以加这些修饰符前缀,默认都是 public
- 默认为 public。外部可访问
- private。外部不可访问,只有内部(这个类自己)的方法可以访问
- protected。只有 类自己 和 类的后代 可以访问
- 也可加 readonly 修饰符,使得属性无法被后续修改
注意下面这两种写法都可以:
1 | // 写法一 |
getter 和 setter
可以把属性写成 pravite 的,然后定义 getter 和 setter 进行属性的读改
用法:用 get
和 set
,一般会和 pravite 属性配合使用
1 | class Person3 { |
静态属性
属性加 static 前缀。静态属性存在于类本身上面而不是类的实例上,但是实例会用到这个属性,访问方法是 类.静态属性
1 | class ChenFamilyPeo { |
应用举例:TS 中的单例模式
1 | class SingleObj { |
抽象类 (少用)
抽象类做为其它派生类的基类使用(抽象子类 === 派生类)。 它们一般不会直接被实例化。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法
并且派生类一定要有抽象类中定义的属性和方法。
1 | abstract class Department { |
函数
函数类型
1 | // 函数定义类型 |
可选参数 & 默认参数
- TS 中参数是必须的,不能是 undifined,不能多也不能少。可以用
?
表示可选参数 - 默认参数同 ES6 如果位置靠前但是想启用默认参数,则传 undefined 即可
当我们想传好几个参数,但是参数不是必选的时候,有时候执行起来会不太优雅:
1 | const testFn = (a?: number, b?: number, c?: number ) => { |
可以改成这种对象的写法:
1 | const testFn = (ops: { a?: number, b?: number, c?: number }) => { |
函数重载
个人理解函数重载就是:函数根据传入不同的参数而返回不同类型的数据的场景 / 执行不同的逻辑。
在 TS 当中,除了在函数内部写判断类型并具体执行的逻辑之外,还要在函数之前 为同一个函数提供多个函数类型定义来进行函数重载
1 | function pickCard(x: {suit: string; card: number }[]): number |
在定义重载的时候,一定要把最精确的定义放在最前面
泛型
泛型,泛指的类型
个人理解:泛型主要是用来提高数据类型方面的复用性:组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,比单纯写 string | number 等这种联合类型写法复用性更高
进一步理解:泛形应该理解为一个储存池,用来保存可能会用到的类型,相当于加了若干个类型参数。捕获输入类型只是泛型的常见应用
类型变量 T
语法是 <T>
,可以帮助我们捕获用户传入的类型,eg:定义一个会返回任何传入它的值的函数
1 | function identity<T>(arg: T): T { // 编译器会认为 T 是任意类型,所以该函数具有强的通用性 |
我们把这个版本的 identity 函数叫做泛型,因为它可以适用于多个类型。可以使用泛型来创建可重用的组件
我们可以这么用该函数
1 | let str = identity('jacle') // 编译器会根据传入的参数自动地帮助我们确定 T 的类型 |
泛型类型 & 接口 & 类
1 | function rec<T>(arg: T): T { |
泛型接口
1 | interface GenericIdentityFn<T> { |
泛型类,与接口类似
1 | function join<T>(a: T, b: T) { |
泛型约束
泛型约束是一个很常用的用法,用来把类型约束成一个更严谨的类型 (使得泛型收窄),eg. 用来确保属性存在。其实泛型约束的方法挺多:extends这种简单的也算
1 | function rec<T>(arg: T): T { |
所以我们可以定义一个接口做一些约束
1 | interface lengthwise { |
keyof
keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。eg:
1 | interface Person { |
所以有了 keyof 之后我们可以更愉快地和 泛型约束 玩耍
1 | function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { |
开发中还遇到不懂的泛型骚操作,详见 一文读懂 TypeScript 泛型及应用
条件类型
有时可发挥类似 interface 的作用
1 | const FeatureContext = React.createContext<{ |
高级类型 (其实用的还挺多的)
交叉类型
交叉类型是将多个类型合并为一个类型。用 & 产生
联合类型
联合类型表示一个值可以是几种类型之一。用 | 产生
类型保护
通俗版
个人理解:类型保护一般出现在有联合类型的地方,类型保护就是用来判断变量类型的,并且在对应的 if 分支内能确定变量类型,进而能提前调用那种类型的函数 (比如提前判定是 string,然后在那个分支就可以用 .substr() )
类型保护的方式
- 类型断言
- in语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24interface Bird {
fly: boolean
sing: () => {}
}
interface Dog {
fly: boolean
bark: () => {}
}
// 类型断言的类型保护
function trains1(animal: Bird | Dog) {
if(animal.fly) {
(animal as Bird).sing()
} else {
(animal as Dog).bark()
}
}
// in 语法的类型保护
function trains2(animal: Bird | Dog) {
if('sing' in animal) {
animal.sing()
} else {
animal.bark()
}
} - typeof
- instanceof
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// typeof 和 instanceof 语法的类型保护
function add(first: string | number, second: string | number) {
if(typeof first === 'string' || typeof second === 'string') {
return `${first}${second}`
}
return first + second
}
// instanceof 语法的类型保护 ,只用于能用 instanceof 的,eg. class (interface不能)
class numberObj {
count: number
constructor(count: number) {
this.count = count
}
}
function add2(first: object | numberObj, second: object | numberObj) {
// 这里不能写成 first instanceof numberObj === true
if(first instanceof numberObj && second instanceof numberObj) {
return first.count + second.count
}
return 0
}
官方版
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型
定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词:这里 pet is Fish
就是类型谓词
就是当下面这个函数返回true的时候,就会有 pet is Fish
,在下面的 if 中相当于一种类型断言的作用了。所以这种函数类型其实也是一种类型保护,或者说是一种底层是类型断言的类型保护
1 | class Fish { |
字符串字面量类型
字符串字面量类型允许你指定字符串必须具有的确切值.
字符串字面量 === 字符串组成的联合类型
1 | // 你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误 |
Typescript 中的 Record, Partial, Readonly , Pick
命名空间
工程化
单文件
使用方法。编译代码:在命令行用 tsc <文件名> 的方式编译 .ts 文件,然后用 node 执行生成的js文件
tsconfig.json 配置文件及其常用语法
在项目当中,根目录运行 tsc --init
能生成该项目的 TS 配置文件: tsconfig.json
运行方法
在根目录命令行单纯地执行 tsc ,会根据配置文件编译整个目录下所有的 .ts 文件(用ts-node ${fileName} 其实也会用这个配置文件) ;想只编译某些文件,可以 files / include 或 exculde (数组写法)eg.
1 | { |
compilerOptions
常用配置项:
- 常规
- removeComments: true 编译时去除注释
- Incremental: true 上次编译过并且没变化的文件这次不会再编译
- JS
- allowJs: true 允许编译javascript文件(转成ES5)
- checkJs: true 检查JS语法
- 检查
- noImplicitAny: true 当一个变量是 any 的时候,必须显示地写出来,不然会报错
- strictNullChecks: true 在严格的 null检查模式下, null和 undefined值不包含在任何类型里,只允许用它们自己和 any来赋值
- 输入输出
- outDir: “./build”
- rootDir: “./src” 此时src文件外不能有 .ts 文件
- 额外检查
- noUnusedLocals 若有未使用的局部变量则抛错
- noUnusedParameters 同理,函数参数
杂
- 项目的script命令中写成 tsc -w ,这个 -w 能监控项目中 TS 文件的变化,有变化就自动重新编译
参考资料
TypeScript 入门教程
TypeScript 官方文档
慕课网:基于 TypeScript 从零重构 axios
慕课网:TypeScript-系统入门到项目实战