基础类型
TypeScript 中的基础类型是在 JavaScript 基础上进行拓展的,包含了三个新的类型
void
表示一个值不存在,用于函数的返回值enum
枚举类型分为三类,字符型,数字型,异构型,他们都会被编译成对象,使用方法如下typescriptenum Foo { A = 1, B = 2, C = "3", } const num: Foo = Foo.A;
其中
Foo
被称作联合枚举类型,他的字段Foo.A
被称作联合枚举成员类型字面量
字面量类型都只有一个可能的值,即字面量本身,比如true
,'hello'
这里补充一下单例类型(单元类型),因为仅包含一个可能的值,因此被称作单例类型,比如 undefined
,void
,null
,字面量
等,实际上这个是另一个维度的分类,因此可能和上面有重叠。
顶端(尾端)类型
顶端类型是所有类型的父类型,在 TypeScript 中包含两种顶端类型
any
因为是所有类型的父类型,因此可以将任意类型的变量赋值给一个 any
类型的变量,同时 any
类型变量也可以赋值给任意类型的其他变量(包含他自身),因此可以合理的利用这个类型来跳过类型检查。
同时,如果一个变量无法推导出类型,默认也会变成 any
类型。为了避免滥用,可以适当调整编译选项,具体见 TypeScript 编译配置
unknown
这是更安全的一种顶端类型,我们可以将任意类型赋值给 unknown
类型变量(与 any
相同),但是只能将 unknown
赋值给 any
和他自身。
在执行一些算术操作时候,也必须要先细化类型,否则会爆出编译错误。
尾端类型是其他所有类型的子类型,因此尾端类型不存在一个值来表示,只有它自身可以给它赋值,即使是 any
类型也无法赋值给它。TypeScript 中仅有一个尾端类型 never
。
never
尾端类型主要在函数返回值上发光发热,他表示函数不可能返回值,这和 void
有区别,前者意思为函数不可能结束(由于异常,或者死循环),后者表示函数可以执行完,但是没有返回值。
还有一个地方也会用到 never
,就是类型体操……有一些工具类型会用到它,比如 Exclude<T, U> = T extends U ? never : T
数组类型
主要是有两种声明方式,简单类型的数组可以直接通过方括号来定义,比较复杂的数组类型倾向于使用泛型来定义。
这里还涉及只读类型数组的定义,有三种方法:
使用
ReadonlyArray<T>
来定义使用
readonly
修饰符使用
Readonly<T>
工具类型来定义,这个工具类型定义如下:typescripttype Readonly<T> = { readonly [K in keyof T]: T[K]; };
他只可以用用在对象身上,因此定义一个只读数组的代码如下
Readonly<number[]>
元组类型
元组是由有限个元素组成的有序列表,一个简单定义(经典元组)如下:
typescript
const point: [number, number] = [0, 0];
我们还可以使用问号来表示可选元素(和 JavaScript 一样),或者使用剩余元素语法:
typescript
const point: [number, number, number?]; // 使用可选元素语法
const tuple: [number, ...string[]]; // 使用剩余元素语法
注意拓展操作符必须在元组类型中才能使用,常用于类型体操的 concat
操作,目前看来只有元组有这个拓展语法,对象似乎没有(不像 JavaScript 一样)
在获取一个元组类型的对象的长度时,TypeScript 编译器会尽可能的利用元组的定义信息来推断长度,比如上面的 point
的长度的类型就是一个字面量的联合类型 2|3
。
同时注意,元组类型是数组类型的子类型,因此可以赋值给数组类型,反之不可以
对象类型
TypeScript 中提供了多种定义对象的方法,我们先介绍三种对象类型:
Object
类型object
类型- 对象字面量
Object
类型
注意这里的 Object
是大写的,但这并不意味着这是一个构造函数的类型,我们首先将这两个区分一下
typescript
const o = new Object();
const p: Object;
首先 o
变量他是 Object
的类型,而构造函数 Object
的类型是 ObjectConstructor
,这个构造函数变量的定义如下:
typescript
interface ObjectConstructor {
readonly prototype: Object;
// ...
}
declare var Object: ObjectConstructor;
可以看到 Object
构造函数的原型对象 prototype
是 Object
类型
从这里我们简单引出类型 Object
的定义,它实际上是 Object
构造函数的原型对象的类型,尽量不要用在其他的变量上。他的类型定义为
typescript
interface Object {
constructor: Function;
toString(): string;
valueOf(): Object;
// ...
}
Object
类型兼容性很好(或者说太过宽泛了……),除了 null
和 undefined
都可以赋值给他,基本类型也可以,这是因为基本类型在赋值的时候可能有一个装包(封装为对应类型的对象)的过程。
所以我们在描述一个对象的类型的时候,尽量不要使用 Object
,他只适合用来描述 Object.prototype
这一个变量
object
类型
注意这里开头为小写,它强调变量类型为非原始类型,但它并不关心变量内部的字段有什么。
在类型兼容方面,任何对象和顶端类型都可以赋值给他,因此范围在 Object
的基础上缩小了。
对象字面量
首先说一下对象字面量的语法,我们先写出一个大括号,然后内部再依次写下属性的类型成员,类型成员包括 key
和 type
,共有五种:
属性签名
PropertyName: Type
,其中PropertyName
也可以是一个计算属性,只需要保证类型是string | number | Symbol
就可以这些属性还可以设置是否只读,是否可选这两种选项。如果一个对象至少有一个属性,并且所有的属性都是可选的,那么我们称呼他为
弱类型
,也许这个弱类型很适合作为用户输入来表示。调用签名
{(paramList): Type}
因为函数实质上也是一个对象(唯一区别是这个对象可以被调用),因此可以写一个调用签名来表示这个对象可以调用。而函数类型字面量实质上也与仅包含一个调用签名的对象字面量等价,前者是后者的简写罢了typescript{(ParamList): Type} // 等价于 (ParamList) => Type
前者的表现力更强,可以描述一个函数的更多信息,比如 vue 中就有用到这种类型声明。
构造签名
new (paramList): Type
其实同上,他也是构造函数字面量的复杂写法。方法签名
PropertyName(ParamList): Type
这个和调用签名是不一样的,调用签名是描述对象本身可以被调用的形式,而方法签名描述的函数类型的属性成员。索引签名
[indexName: string]: Type
这里面的indexName
是索引名,但实际上并没有什么作用,真正起作用的是后面标注的索引类型string
表示这个对象的索引必须是字符串类型,同理,除了字符串类型,还有数字类型可以选择但注意,JavaScript 中表面看有整数索引,但实际上是将整数转换成字符串
函数类型
我们这里不介绍基础的内容,先讲一下函数重载,先看一个实例:
typescript
function f(x: string): 0 | 1;
function f(x: any): number;
function f(x: any): any {
// ...
}
要注意前两行代码是函数重载,最后一行是函数定义,函数定义的类型需要兼容所有函数重载。
而且函数重载必须紧挨着,不能有空行之类的(会编译错误),他们的顺序也是有要求的,必须精确的重载在前,模糊的在后
所以现在看来 TypeScript 的函数重载并不是很实用。还不如如下的定义:
typescript
{
(x: string): 0|1;
(x: any): number;
}
我们仅定义一个对象,给他两个调用签名。
除了函数重载,还可以定义 this
的类型,例子如下:
typescript
function add(this: void, x: number, y: number) {
this.name = "sth"; // error
}
我们可以这里函数定义多了一个 this
,这不意味着我们在调用时需要传入一个 this 参数,编译器会自动清除这个参数,他只是提供类型检查信息的。
接口类型
接口类型和对象字面量类型很相似,只是在对象字面量前面加上了 interface
关键字和一个接口名(接口名首字母大写),其内部属性的定义也是一样的,包含那五种类型成员。因此我们这里只介绍接口特有的功能。
继承
接口可以继承以下类型
- 接口
- 对象类型的类型别名
- 类
- 对象类型的交叉类型
继承会使用 extends
关键字,并且一个接口可以继承多个目标。但这个继承的过程可能发生字段的冲突,我们思考一下
父子接口的字段冲突:
如果发生冲突,子接口的相同字段有更高的优先级,但需要保证子接口的类型能够赋值给父接口的同名字段,比如
typescriptinterface A { o: { a: number }; } interface B extends A { o: { a: number; b: number }; }
多个父接口之间的冲突
如果发生冲突,父接口之间的成员类型必须是完全相同的,否则会无法编译(报错)
但不是没有解决方案,我们可以在子接口上定义同名字段,这个同名字段同时可以赋值给所有包含该字段的父接口,相当于做了一个垫片。
typescriptinterface A { o: { a: number }; } interface B { o: { hello: string }; } interface C extends A, B { o: { a: number; hello: string }; }
类型别名
类型别名和接口很相似,接口是给对象字面量起个名字,类型别名是给任意类型起个名字。类型别名的首字母同样需要大写。我们只谈论类型别名特有的功能
递归类型别名
三种情况下可以递归的使用类型别名
- 别名引用的是对象字面量,接口或者函数字面量
- 别名引用的是一个数组
- 别名引用的是一个泛型类或者接口
我们以 JSON
类型为例,看一下是如何使用的:
typescript
type Json =
| string
| number
| boolean
| null
| { [property: string]: Json }
| Json[];
这里可以看到涉及了第一种和第二种情况
类型别名和接口的区别
- 类型别名可以用在非对象类型上
- 接口可以实现继承,类型别名只能实现类似继承的效果
- 接口名提供的类型诊断信息更清晰,类型别名会显示实际的内容,可能会很杂乱
- 接口有类型合并的效果
类
我们只讲类的一些特殊的语法
类可以实现继承,但是只允许单继承一个类,继承类的时候需要用 super
关键字调用父类的构造函数。类还可以实现接口,对于实现接口的数量没有要求。
类可以定义访问控制等级,比如 private
,protected
,public
,还可以定义是否是静态成员
类本身还可以定义是否是抽象类,具体使用到的时候再讲。