TypeScript入门之元组类型

210 阅读8分钟

简介

元组(tuple)是TypeScript特有的数据类型,JavaScript没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。

元组必须明确声明每个成员的类型。

type tupleType  = [string,string,boolean]
let arr:tupleType = ['1','2',true]

上面示例中,元组tupleType的前两个成员的类型是string,最后一个成员的类型是boolean

元组类型的写法,与数组有一个重大差异。数组的成员类型写在方括号外面(number[]),元组的成员类型是写在方括号里面(number[])。

TypeScript的区分方式是,成员类型写在方括号里面的就是元组,写在外面的就是数组。

// 元组
let tupleArray:[number,string] = [1,'string']
// 数组
let array:(number|string)[] = [1]

上面示例中,变量tupleArray是一个元组,有两个类型numberstring

使用元组时,必须明确给出类型声明(上例的[number,string]),不能省略,否则TypeScript会把一个值自动推断为数组。

// 未声明类型,默认推断为数组,类型(number|boolean|string)[]
let arr = [1,true,'string']

上面示例中,变量arr的值其实是一个元组,但是TypeScript会将其推断为一个联合类型的数组,即arr的类型为(number|boolean|string)[]

元组成员的类型可以添加问号后缀(?),表示该成员是可选的

let arr:[number,string?]  = [1] // 正常

let abs:[number,string] = [1] // 报错

上面示例中,元组arr的第二个成员是可选的,可以省略。

注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。

// 正常
type Tuple = [
  number,
  string,
  boolean?
];
// 必需元素不能跟在可选元素后面
// A required element cannot follow an optional element
type TupleCopy = [
  number?,
  string
]

上面示例中,元组Tuple的最后成员是可选的。也就是说,它可以存在可以不存在并不会报错。

由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。

let arr:[string,number] = ['a',1]

// 报错
// Type '"c"' is not assignable to type 'undefined'.
// Tuple type '[string, number]' of length '2' has no element at index '2'.
arr[2] = 'c'

上面示例中,变量arr是一个只有两个成员的元组,如果对第三个成员赋值就报错了。

如果,想成为不限成员数量的元组,可以使用(...)扩展运算符。

type NamedNums = [
  string,
  ...number[],
]
const a:NamedNums = ['string',1,2]
const b:NamedNums = ['string',1,2,3]

上面示例中,元组类型NamedNums的第一个成员是字符串,后面的成员使用扩展运算符来展开一个数组,从而实现了不定数量的成员。

扩展运算符用在元组的任意位置都可以,但是它后面只能是数组或元组。

type T1 = [string,number,...boolean[]]
let arr:T1 = ['string',1,true,false]

type T2 = [string,...number[],boolean]
let arr:T2 = ['string',1,2,false]

type T3 = [...[string],number,boolean]
let arr = ['string','string1',1,true]

上面示例中,扩展运算符分别在元组的尾部、中部和头部。

如果不确定元组成员的类型和数量,可以写成下面这样。

type Tuple = [...any[]]

let arr:Tuple = [1,2,'String',true,false]

上面示例中,元组Tuple可以放置任意数量和类型的成员。但是这样写,也就失去了使用元组和TypeScript的意义。

元组可以通过方括号,读取成员类型。

type Tuple2 = [string,number]

type Name = Tuple2[0] // string
type Age = Tuple2[1] // number

上面示例中,Tuple[0]返回0号位置的成员类型,Tuple[1]返回1号位置的成员类型。

由于元组的成员都是数值索引,即索引类型都是number,所以可以像下面这样读取。

type Tuple = [string,number,Date]
type TupleEl = Tuple[number]; // string | number | Date

let string:TupleEl = 'STRING' 
let number:TupleEl = 111 
let date:TupleEl = new Date() 

上面示例中,Tuple[number]表示元组Tuple的所有数值索引的成员类型,所以返回string|number|Date,即这个类型是三种值的联合类型。

只读元组

元组也可以是只读的,不允许修改,有两种写法。

type Readonly = readonly[number,string]
let arr:Readonly = [1,'string']

type ReadonlyGenerics = Readonly<[number,string]>
let arr:ReadonlyGenerics = [1,'string']

// Cannot assign to '0' because it is a read-only property.
// 不能赋值给'0',因为它是一个只读属性。
arr[0] = 1; // 报错

上面示例中,两种写法都可以得到只读元组,其中写法二是一个泛型,用到了工具类型Readonly<T>

跟数组一样,只读元组是元组的父类型。所有,元组可以替代只读元组,而只读元组不能替代元组。

type t1 = readonly[number,string]
type t2 = [number,string]


let t2x:t2 = [1,'string']
let t1y:t1 = t2x;

t2x = t1y // 报错

上面示例中,类型t1是只读元组,类型t2是普通元组。t2类型可以赋值给t1类型,反过来就会报错。

由于只读元组不能替代元组,所以会产生一些令人困惑的报错。

const sum = ([x,y]:[number,number])=>{
  return x + y
}

let point = [3,4] as const; // 只读类型的另一种声明方式
sum(point) // 报错

上面示例中,函数sum()的参数是一个元组,传入只读元组就会报错,因为只读元组不能替代元组。

上面示例报错的解决方式有两种,就是使用类型断言,一种类型声明标注好,另一种类型断言为普通元组。

const sum = ([x,y]:[number,number])=>{
  return x + y
}

let point:[number,number] = [3,4] as const; // 正常 类型声明
sum(point as [number,number]) // 类型断言

成员数量的推断

如果没有可选成员和扩展运算符,TypeScript会推断出元组的成员数量(即元组长度)。

const fn = (point:[number,number])=>{
  // This comparison appears to be unintentional because the types '2' and '3' have no  overlap.
  // 这种比较似乎是无意的,因为类型'2'和'3'没有重叠。
  if(point.length === 3){
    // ...
  }
}

上面示例会报错,原因是TypeScript发现元组point的长度是2,不可能等于3,这个判断无意义。

如果包含了可选成员,TypeScript会推断出可能的成员数量。

const fn = (point:[number,number?,number?])=>{
  // This comparison appears to be unintentional because the types '1 | 3 | 2' and '4' have no overlap.
  // 这种比较似乎是无意的,因为类型'1 | 3 | 2'和'4'没有重叠。
  if(point.length === 4){
    // ...
  }
}

上面示例会报错,原因是 TypeScript 发现point.length的类型是1|2|3,不可能等于4

如果使用了扩展运算符,TypeScript 就无法推断出成员数量。

const Tuple:[...string[]] = ['1','2','3'] 
// 正常
if(Tuple3.length === 4){
  // ...
}

上面示例中,Tuple只有三个成员,但是 TypeScript 推断不出它的成员数量,因为它的类型用到了扩展运算符,TypeScript 把Tuple当成数组看待,而数组的成员数量是不确定的。

一旦扩展运算符使得元组的成员数量无法推断,TypeScript 内部就会把该元组当成数组处理。

扩展运算符与成员数量

扩展运算符(...)将数组(注意,不是元组)转换成一个逗号分隔的序列,这时 TypeScript 会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的。

这导致如果函数调用时,使用扩展运算符传入函数参数,可能发生参数数量与数组长度不匹配的报错。

const arr = [1,2]

const add = (x:number,y:number)=>{
  // ...
}
// A spread argument must either have a tuple type or be passed to a rest parameter.
// 扩展参数必须具有元组类型,或者传递给rest形参。以上翻译结果来自有道神经网络翻译(YNMT)· 通用场景
add(...arr) // 报错

上面示例会报错,原因是函数add()只能接受两个参数,但是传入的是...arr,TypeScript 认为转换后的参数个数是不确定的。

上面示例中,console.log()可以接受任意数量的参数,所以传入...arr就不会报错。

解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符。

let arr:[number,number] = [1,2]

const add = (x:number,y:number)=>{
  return x * y
}
add(...arr)

上面示例中,arr是一个拥有两个成员的元组,所以 TypeScript 能够确定...arr可以匹配函数add()的参数数量,就不会报错了。

另一种写法是使用as const断言。

let arr = [1,2] as const

const add = (x:number,y:number)=>{
  return x * y
}
add(...arr)

上面这种写法也可以,因为 TypeScript 会认为arr的类型是readonly [1, 2],这是一个只读的值类型,可以当作数组,也可以当作元组。

结尾

上述摘要来自阮一峰老师的TS教程,好记性不如烂笔头,自己梳理一遍增加记忆力,每日更新自己的知识库。