1、元组
元组可以看做是数组的拓展,它表示已知元素数量和类型的数组。确切地说,是已知数组中每一个位置上的元素的 类型,来看例子:
let tuple: [string, number, boolean];
tuple = ["a", 2, false];
tuple = [2, "a", false]; // error 不能将类型“number”分配给类型“string”。 不能将类型“string”分配给类型“number”。
tuple = ["a", 2]; // error Property '2' is missing in type '[string, number]' but required in type '[string, number, boolean]'
可以看到,上面我们定义了一个元组 tuple,它包含三个元素,且每个元素的类型是固定的。当我们为 tuple 赋值 时:各个位置上的元素类型都要对应,元素个数也要一致。我们还可以给单个元素赋值:
tuple[1] = 3;
这里我们给元组 tuple 的索引为 1 即第二个元素赋值为 3,第二个元素类型为 number,我们赋值给 3,所以没有问题。当我们访问元组中元素时,TypeScript 会对我们在元素上做的操作进行检查:
tuple[0].split(":"); // right 类型"string"拥有属性"split"
tuple[1].split(":"); // error 类型“number”上不存在属性“split”
上面的例子中,我们访问的 tuple 的第二个元素的元素类型为 number,而数值没有 split 方法,所以会报错。在 2.6 版本之前,TypeScript 对于元组长度的校验和 2.6 之后的版本有所不同,我们来看下面的例子,前后版本对 于该情况的处理:
let tuple: [string, number];
tuple = ["a", 2]; // right 类型和个数都对应,没问题
// 2.6版本之前如下也不会报错
tuple = ["a", 2, "b"];
// 2.6版本之后如下会报错
tuple = ["a", 2, "b"]; // error 不能将类型“[string, number, string]”分配给类型“[string, number]”。 属性“length”的类型不兼容。
这个赋给元组的值有三个元素,是比我们定义的元组类型元素个数多的:
- 在 2.6 及之前版本中,超出规定个数的元素称作越界元素,但是只要越界元素的类型是定义的类型中的一种即 可。比如我们定义的类型有两种:string 和 number,越界的元素是 string 类型,属于联合类型 string | number , 所以没问题,联合类型的概念我们后面会讲到。
- 在 2.6 之后的版本,去掉了这个越界元素是联合类型的子类型即可的条件,要求元组赋值必须类型和个数都对应。
在 2.6 之后的版本,[string, number]元组类型的声明效果上可以看做等同于下面的声明:
interface Tuple extends Array<number | string> {
0: string;
1: number;
length: 2;
}
上面这个声明中,我们定义接口 Tuple ,它继承数组类型,并且数组元素的类型是 number 和 string 构成的联合类型,这样接口 Tuple 就拥有了数组类型所有的特性。并且我们明确指定索引为0的值为 string 类型,索引为1的值为 number 类型,同时我们指定 length 属性的类型字面量为 2,这样当我们再指定一个类型为这个接口 Tuple 的时 候,这个值必须是数组,而且如果元素个数超过2个时,它的length就不是2是大于2的数了,就不满足这个接口定义 了,所以就会报错;当然,如果元素个数不够2个也会报错,因为索引为0或1的值缺失。
如果你想要和 2.6 及之前版本一样的元组特性,那你可以这样定义接口:
interface Tuple extends Array<number | string> {
0: string;
1: number;
}
也就是去掉接口中定义的 length: 2 ,这样 Tuple 接口的 length就是从 Array继承过来的 number 类型,而不用必须是 2 了。
2、枚举
enum 类型在 C++这些语言中比较常见,TypeScript 在 ES 原有类型基础上加入枚举类型,使我们在 TypeScript 中 也可以给一组数值赋予名字,这样对开发者来说较为友好。比如我们要定义一组角色,每一个角色用一个数字代 表,就可以使用枚举类型来定义:
enum Roles {
SUPER_ADMIN,
ADMIN,
USER
}
上面定义的枚举类型 Roles 里面有三个值,TypeScript 会为它们每个值分配编号,默认从 0 开始,依次排列,所以 它们对应的值是:
enum Roles {
SUPER_ADMIN = 0,
ADMIN = 1,
USER = 2
}
当我们使用的时候,就可以使用名字而不需要记数字和名称的对照关系了:
const superAdmin = Roles.SUPER_ADMIN;
console.log(superAdmin); // 0
你也可以修改这个数值,比如你想让这个编码从 1 开始而不是 0,可以如下定义:
enum Roles {
SUPER_ADMIN = 1,
ADMIN,
USER
}
这样当你访问 Roles.ADMIN时,它的值就是 2 了。你也可以为每个值都赋予不同的、不按顺序排列的值:
enum Roles {
SUPER_ADMIN = 1,
ADMIN = 3,
USER = 7
}
通过名字 Roles.SUPER_ADMIN 可以获取到它对应的值 1,同时你也可以通过值获取到它的名字,以上面任意数值 这个例子为前提:
console.log(Roles[3]); // 'ADMIN'
3、Any
JavaScript 的类型是灵活的,程序有时也是多变的。有时,我们在编写代码的时候,并不能清楚地知道一个值到底 是什么类型,这时就需要用到 any 类型,即任意类型。我们来看例子:
let value: any;
value = 123;
value = "abc";
value = false;
你可以看到,我们定义变量 value,指定它的类型为 any,接下来赋予任何类型的值都是可以的。我们还可以在定义数组类型时使用 any 来指定数组中的元素类型为任意类型:
const array: any[] = [1, "a", true];
但是请注意,不要滥用 any,如果任何值都指定为 any 类型,那么 TypeScript 将失去它的意义。所以如果类型是未知的,更安全的做法是使用unknown类型。
4、void
void 和 any 相反,any 是表示任意类型,而 void 是表示没有任意类型,就是什么类型都不是,这在我们定义函 数,函数没有返回值时会用到:
const consoleText = (text: string): void => {
console.log(text);
};
void 类型的变量只能赋值为 undefined 和 null ,其他类型不能赋值给 void 类型的变量、
5、never
never 类型指那些永不存在的值的类型,它是那些总会抛出异常或根本不会有返回值的函数表达式的返回值类型, 当变量被永不为真的类型保护(后面章节会详细介绍)所约束时,该变量也是 never 类型。这个类型比较难理解,我们先来看几个例子:
const errorFunc = (message: string): never => {
throw new Error(message);
};
这个 errorFunc 函数总是会抛出异常,所以它的返回值类型时 never,用来表明它的返回值是永不存在的。
const infiniteFunc = (): never => {
while (true) {}
};
infiniteFunc 也是根本不会有返回值的函数,它和之前讲 void 类型时的 consoleText 函数不同, consoleText 函数没有返回值,是我们在定义函数的时候没有给它返回值,而 infiniteFunc 是死循环是根本不会返回值的,所以它们二者还是有区别的。
never 类型是任何类型的子类型,所以它可以赋值给任何类型;而没有类型是 never 的子类型,所以除了它自身没有任何类型可以赋值给 never 类型,any 类型也不能赋值给 never 类型。我们来看例子:
let neverVariable = (() => {
while (true) {}
})();
neverVariable = 123; // error 不能将类型"number"分配给类型"never"
上面例子我们定义了一个立即执行函数,也就是 "let neverVariable = " 右边的内容。右边的函数体内是一个死循环, 所以这个函数调用后的返回值类型为 never,所以赋值之后 neverVariable 的类型是 never 类型,当我们给 neverVariable 赋值 123 时,就会报错,因为除它自身外任何类型都不能赋值给 never 类型。
6、unknown
unknown类型是TypeScript在3.0版本新增的类型,它表示未知的类型,这样看来它貌似和any很像,但是还是有区别的,也就是所谓的“unknown相对于any是安全的”。怎么理解呢?我们知道当一个值我们不能确定它的类型的时 候,可以指定它是any类型;但是当指定了any类型之后,这个值基本上是“废”了,你可以随意对它进行属性方法的访问,不管有的还是没有的,可以把它当做任意类型的值来使用,这往往会产生问题,如下:
let value: any
console.log(value.name)
console.log(value.toFixed())
console.log(value.length)
上面这些语句都不会报错,因为value是any类型,所以后面三个操作都有合法的情况,当value是一个对象时,访问 name属性是没问题的;当value是数值类型的时候,调用它的toFixed方法没问题;当value是字符串或数组时获取它的length属性是没问题的。
而当你指定值为unknown类型的时候,如果没有通过基于控制流的类型断言来缩小范围的话,是不能对它进行任何操作的,unknown类型的值不是可以随便操作的。
7、高级类型
7.1、交叉类型
交叉类型就是取多个类型的并集,使用 & 符号定义,被&符链接的多个类型构成一个交叉类型,表示这个类型同时 具备这几个连接起来的类型的特点,来看例子:
//交叉类型
interface ClassA{
name:string;
age:number
}
interface ClassB{
name:string;
phone:number;
}
//将接口ClassA和接口ClassB通过&进行合并创建一个新的接口类型Class
type Class = ClassA & ClassB
let info:Class = {
name:'zhagsan',
age:18,
phone:1573875555
}
console.log("交叉结果:",info)//交叉结果: {"name": "zhagsan","age": 18,"phone": 1573875555}
要点说明:
任何类型都能通过&
合并成新的类型吗?
这肯定是不行的,原子类型进行合并是没有任何意义,因为它们合并后的类型是never,比如string&number,这肯定是错误的,因为不可能有既满足字符串又满足数字类型。
合并的接口类型中具有同名属性,该怎么处理?
这里分两种情况,如果同名属性的类型相同则合并后还是原本类型,如果类型不同,则合并后类型为never
interface X{
q:number,
w:string
}
interface Y{
q:boolean,
r:string,
}
type XY = X&Y
编辑器中直接就给我们了提示,如下图所示:
高级示例:
interface A {
inner: D;
}
interface B {
inner: E;
}
interface C {
inner: F;
}
interface D {
d: boolean;
}
interface E {
e: string;
}
interface F {
f: number;
}
type ABC = A & B & C;
let abc: ABC = {
inner: {
d: false,
e: 'className',
f: 5
}
};
7.2、联合类型
联合类型和交叉类型比较相似,联合类型通过|
符号连接多个类型从而生成新的类型。它主要是取多个类型的交集,即多个类型共有的类型才是联合类型最终的类型。联合类型可以是多个类型其中一个,可做选择,比如:string | number
,它的取值可以是string
类型也可以是number
类型。举几个例子,如下所示:
- 声明变量的时候设置变量类型
let a:string|number|boolean;
a = 's';
a = 1;
a= false;
- 多个接口类型进行联合
interface X{
q:number,
w:string,
r:string
}
interface Y{
q:number
r:string,
}
type XY = X | Y
let value:XY = {
q:1,
r:'r'
}
- 函数接口类型进行联合
interface X{
x:()=>string;
y:()=>Number;
}
interface Y{
x:()=>string;
}
type XY = X|Y;
function func1():XY{
//此处不进行类型断言为XY在编辑器中会报类型错误
return {} as XY
}
let testFunc = func1();
testFunc.x();
testFunc.y(); //Error:类型“XY”上不存在属性“y”,类型“Y”上不存在属性“y”。
另外我们还要注意,testFunc.x()
还会报类型错误,我们需要用类型守卫来区分不同类型。这里我们用in
操作符来判断
if('x' in testFunc) testFunc.x()