来做操吧!深入 TypeScript 高级类型和类型体操

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了来做操吧!深入 TypeScript 高级类型和类型体操相关的知识,希望对你有一定的参考价值。

参考技术A TypeScript 给 javascript 扩展了类型的语法,我们可以给变量加上类型,在编译期间会做类型检查,配合编辑器还能做更准确的智能提示。此外,TypeScript 还支持了高级类型用于增加类型系统的灵活性。

就像 JavaScript 的高阶函数是生成函数的函数,React 的高阶组件是生成组件的组件一样,Typescript 的高级类型就是生成类型的类型。

TypeScript 高级类型是通过 type 定义的有类型参数(也叫泛型)的类型,它会对传入的类型参数做一系列的类型计算,产生新的类型。

type Pick =

[P in K]: T[P];

;

比如,这个 Pick 就是一个高级类型,它有类型参数 T 和 K,类型参数经过一系列的类型计算逻辑,会返回新的类型。

TypeScript 高级类型会根据类型参数求出新的类型,这个过程会涉及一系列的类型计算逻辑,这些类型计算逻辑就叫做类型体操。当然,这并不是一个正式的概念,只是社区的戏称,因为有的类型计算逻辑是比较复杂的。

TypeScript 的类型系统是图灵完备的,也就是说它能描述任何可计算逻辑,简单点来说就是循环、条件判断等该有的语法都有。

既然 TypeScript 的类型系统这么强,那我们就做一些高级类型的类型体操来感受下吧。

我们会做这些体操:

用 ts 类型实现加法

用 ts 类型生成重复 N 次的字符串

用 ts 类型实现简易的 js parser(部分)

用 ts 类型实现对象属性按条件过滤

我把这些体操分为数字类的、字符串类的、对象类的,把这三种类型计算逻辑的规律掌握了,相信你的体操水平会提升一截。

TypeScript 类型语法基础

在做体操之前,要先过一下 TypeScript 的类型语法,也就是能做哪些类型计算逻辑。

既然说该有的语法都有,那我们来看下循环和判断都怎么做:

ts 类型的条件判断

图片

ts 类型的条件判断的语法是 条件 ? 分支1 : 分支2 。

extends 关键字是用于判断 A 是否是 B 类型的。例子中传入的类型参数 T 是 1,是 number 类型,所以最终返回的是 true。

ts 类型的循环

图片ts 类型没有循环,但可以用递归来实现循环。

我们要构造一个长度为 n 的数组,那么就要传入长度的类型参数 Len、元素的类型参数 Ele、以及构造出的数组的类型参数 Arr(用于递归)。

然后类型计算逻辑就是判断 Arr 的 length 是否是 Len,如果是的话,就返回构造出的 Arr,不是的话就往其中添加一个元素继续构造。

这样,我们就递归的创建了一个长度为 Len 的数组。

ts 类型的字符串操作

ts 支持构造新的字符串:

图片

也支持根据模式匹配来取字符串中的某一部分:

图片

ts 类型的对象操作

ts 支持对对象取属性、取值:

图片

也可以创建新的对象类型:

图片

通过 keyof 取出 obj 的所有属性名,通过 in 遍历属性名并取对应的属性值,通过这些来生成新的对象类型 newObj。

我们过了一下常用的 ts 类型的语法,包括条件判断、循环(用递归实现)、字符串操作(构造字符串、取某部分子串)、对象操作(构造对象、取属性值)。接下来就用这些来做操吧。

ts 类型体操练习

我们把体操分为 3 类来练习,之后再分别总结规律。

数字类的类型体操

体操 1:实现高级类型 Add,能够做数字加法。

ts 类型能做数字加法么?肯定可以的,因为它是图灵完备的,也就是各种可计算逻辑都可以做。

那怎么做呢?

数组类型可以取 length 属性,那不就是个数字么。可以通过构造一定长度的数组来实现加法。

上文我们实现了通过递归的方式实现了构造一定长度的新数组的高级类型:

type createArray = Arr['length'] extends Len ? Arr : createArray

那只要分别构造两个不同长度的数组,然后合并到一起,再取 length 就行了。

type Add = [...createArray, ...createArray ]['length']

我们测试下:

图片

我们通过构造数组的方式实现了加法!

小结下:ts 的高级类型想做数字的运算只能用构造不同长度的数组再取 length 的方式,因为没有类型的加减乘除运算符。

字符串类的体操

体操2:把字符串重复 n 次。

字符串的构造我们前面学过了,就是通过 $A$B 的方式,那只要做下计数,判断下重复次数就行了。

计数涉及到了数字运算,要通过构造数组再取 length 的方式。

所以,我们要递归的构造数组来计数,并且递归的构造字符串,然后判断数组长度达到目标就返回构造的字符串。

所以有 Str(待重复的字符串)、Count(重复次数)、Arr(用于计数的数组)、ResStr(构造出的字符串)四个类型参数:

type RepeactStr<str p="" string,

Count,

Arr extends Str[] = [],

ResStr extends string = ''>

= Arr['length'] extends Count

? ResStr

: RepeactStr ;

我们递归的构造了数组和字符串,判断构造的数组的 length 如果到了 Count,就返回构造的字符串 ResStr,否则继续递归构造。

测试一下:

图片

小结:递归构造字符串的时候要通过递归构造数组来做计数,直到计数满足条件,就生成了目标的字符串。

这个体操只用到了构造字符串,没用到字符串通过模式匹配取子串,我们再做一个体操。

体操3: 实现简易的 JS Parser,能解析字符串 add(11,22) 的函数名和参数

字符串的解析需要根据模式匹配取子串。这里要分别解析函数名(functionName)、括号(brackets)、数字(num)、逗号(comma),我们分别实现相应的高级类型。

解析函数名

函数名是由字母构成,我们只要一个个字符一个字符的取,判断是否为字母,是的话就记录下该字符,然后对剩下的字符串递归进行同样的处理,直到不为字母的字符,通过这样的方式就能取出函数名。

我们先定义字母的类型:

type alphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'

| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'

| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'

| 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';

还有保存中间结果的类型:

type TempParseResult =

token: Token,

rest: Rest



然后就一个个取字符来判断,把取到的字符构造成字符串存入中间结果:

type parseFunctionName

= SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends alphaChars

? parseFunctionName

: TempParseResult

: never;

我们取了单个字符,然后判断是否是字母,是的话就把取到的字符构造成新的字符串,然后继续递归取剩余的字符串。

测试一下:

图片

图片

符合我们的需求,我们通过模式匹配取子串的方式解析出了函数名。

然后继续解析剩下的。

解析括号

括号的匹配也是同样的方式,而且括号只有一个字符,不需要递归的取,取一次就行。

type brackets = '(' | ')';

type parseBrackets

= SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends brackets

? TempParseResult

: never

: never;

测试一下:

图片

继续解析剩下的:

解析数字

数字的解析也是一个字符一个字符的取,判断是否匹配,匹配的话就递归取下一个字符,直到不匹配:

type numChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

type parseNum

= SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends numChars

? parseNum

: TempParseResult

: never;

测试一下:

图片

继续解析剩下的:

解析逗号

逗号和括号一样,只需要取一个字符判断即可,不需要递归。

type parseComma

= SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends ','

? TempParseResult

: never

: never;

测试一下:

图片

至此,我们完成了所有的字符的解析,解析来按照顺序组织起来就行。

整体解析

单个 token 的解析都做完了,整体解析就是组织下顺序,每次解析完拿到剩余的字符串传入下一个解析逻辑,全部解析完,就可以拿到各种信息。

type parse

= parseFunctionName extends TempParseResult

? parseBrackets extends TempParseResult

? parseNum extends TempParseResult

? parseComma extends TempParseResult

? parseNum extends TempParseResult

? parseBrackets extends TempParseResult

?

functionName: FunctionName,

params: [Num1, Num2],

: never: never: never: never : never : never;

测试一下:

图片

大功告成,我们用 ts 类型实现了简易的 parser!

小结:ts 类型可以通过模式匹配的方式取出子串,我们通过一个字符一个字符的取然后判断的方式,递归的拆分 token,然后按照顺序拆分出 token,就能实现字符串的解析。

完整代码如下:

type numChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

type alphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'

| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'

| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'

| 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';

type TempParseResult =

token: Token,

rest: Rest



type parseFunctionName =

SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends alphaChars

? parseFunctionName

: TempParseResult

: never;

type brackets = '(' | ')';

type parseBrackets

= SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends brackets

? TempParseResult

: never

: never;

type parseNum

= SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends numChars

? parseNum

: TempParseResult

: never;

type parseComma

= SourceStr extends `$infer PrefixChar$infer RestStr`

? PrefixChar extends ','

? TempParseResult

: never

: never;

type parse

= parseFunctionName extends TempParseResult

? parseBrackets extends TempParseResult

? parseNum extends TempParseResult

? parseComma extends TempParseResult

? parseNum extends TempParseResult

? parseBrackets extends TempParseResult

?

functionName: FunctionName,

params: [Num1, Num2],

: never: never: never: never : never : never;

type res = parse;

对象类的体操

体操4:实现高级类型,取出对象类型中的数字属性值

构造对象、取属性名、取属性值的语法上文学过了,这里组合下就行:

type filterNumberProp =

[Key in keyof T] : T[Key] extends number ? T[Key] : never

[keyof T];

我们构造一个新的对象类型,通过 keyof 遍历对象的属性名,然后对属性值做判断,如果不是数字就返回 never,然后再取属性值。

属性值返回 never 就代表这个属性不存在,就能达到过滤的效果。

测试一下:

图片

小结:对象类型可以通过 构造新对象,通过 [] 取属性值,通过 keyof 遍历属性名,综合这些语法就可以实现各种对象类型的逻辑。

总结

TypeScript 给 JavaScript 扩展了类型的语法,而且还支持了高级类型来生成类型。

TypeScript 的类型系统是图灵完备的,可以描述任何可计算逻辑:

有 ? : 可以做条件判断,常配合 extends 使用

通过递归可以实现循环

可以做对象的构造 、取属性名 keyof、取属性值 T[Key]

可以做字符串的构造 $a$b,字符串的模式匹配来取子串 str extends $infer x$infer y

我们分别做了这些类型体操:

ts 实现加法:通过递归构造数组再取长度

ts 实现重复字符串:递归构造数组来计数,然后递归构造字符串

ts 实现 parser:通过字符串模式匹配取子串的方式来解析每一部分,最后组合调用

ts 实现对象属性过滤:通过构造对象、取属性名、取值的语法组合调用

其中要注意的就是数字类的要通过构造数组取长度的方式来计算,再就是字符串的模式匹配结合 infer 保存中间结果来取子串,这两个是相对难度大一些的。

其实各种高级类型,只要熟悉了 ts 类型语法,想清楚了逻辑就能一步步写出来,和写 JS 逻辑没啥本质区别,只不过它是用于生成类型的逻辑。

读到这里,是不是感觉高级类型的类型体操也没有啥难度了呢?

TypeScript深入学习TypeScript对象类型

@TOC

前言

订阅专栏关注博主,学习TypeScript不迷路!

好嘞,言归正传,让我们开始深入学习TypeScript对象类型吧:

interface PaintOptions 
    x?: number;
    y?: number;

使用接口定义了一个对象类型,其中的属性都为可选属性,在[【TypeScript】TypeScript常用类型(上篇)]中我们已经知道不能够直接使用可选属性,需要先对其进行判空操作

function ObjFn(obj: PaintOptions) 
    if (obj.x && obj.y)  // 对可选属性进行存在性判断
        console.log(obj.x + obj.y);
    

其实这不是唯一的方式,我们也可以对可选属性设置个默认值,当该属性不存在时,使用我们设置的默认值即可,看下面这个例子:

function ObjFn( x = 1, y = 2 : PaintOptions) 
     console.log(x + y);


ObjFn( x: 4, y: 5 ); // log: 9
ObjFn(); // log: 3

在这里,我们为 ObjFn的参数使用了一个解构模式,并为 xy提供了默认值。现在xy 都肯定存在于 ObjFn的主体中,但对于 ObjFn的任何调用者来说是可选的。

只读属性

TypeScript中使用readonly修饰符可以定义只读属性:

interface NameType 
    readonly name: string; // 只读属性

function getName(obj: NameType) 
    // 可以读取 obj.name.
    console.log(obj.name);
    // 但不能重新设置值
    obj.name = "Ailjx";


readonly修饰符只能限制一个属性本身不能被重新写入,对于复杂类型的属性,其内部依旧可以改变:

interface Info 
    readonly friend: string[];
    readonly parent:  father: string; mother: string ;


function getInfo(obj: Info) 
    // 正常运行
    obj.friend[0] = "one";
    obj.parent.father = "MyFather";
    // 报错
    obj.friend = ["one"];
    obj.parent =  father: "MyFather", mother: "MyMother" ;


TypeScript在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是
否是 readonly ,所以 readony 属性也可以通过别名来改变:

interface Person 
    name: string;
    age: number;

interface ReadonlyPerson 
    readonly name: string;
    readonly age: number;

let writablePerson: Person = 
    name: "AiLjx",
    age: 18,
;
// 正常工作
let readonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); // 打印 18
// readonlyPerson.age++; // 报错
writablePerson.age++;
console.log(readonlyPerson.age); // 打印 19

这里有点绕,我们来梳理一下:

  • 首先我们声明了两个几乎相同的接口类型PersonReadonlyPerson ,不同的是ReadonlyPerson 里的属性都是只读的。

  • 之后我们定义了一个类型为Person的变量writablePerson,可知这个变量内的属性的值是可修改的。

  • 接下来有意思的是writablePerson竟然能够赋值给类型为ReadonlyPerson的变量readonlyPerson,这就验证了TypeScript在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是否是 readonly ,所以类型为PersonReadonlyPerson的数据可以相互赋值。

  • 此时要明白变量readonlyPerson里面的属性都是只读的,我们直接通过readonlyPerson.age++修改age是会报错的,但有意思的是我们可以通过writablePerson.age++修改writablePerson中的age,又因为对于引用类型的数据来说直接赋值就只是引用赋值(即浅拷贝),所以writablePerson变化后readonlyPerson也跟着变化了
  • 这样readonlyPerson中的只读属性就成功被修改了

索引签名

在一些情况下,我们可能不知道对象内所有属性的名称,那属性名称都不知道,我们该怎么去定义这个对象的类型呢?

这时我们可以使用一个索引签名来描述可能的值的类型:

interface IObj 
    [index: string]: string;

const obj0: IObj = ;
const obj1: IObj =  name: "1" ;
const obj2: IObj =  name: "Ailjx", age: "18" ;
  • 上面就是使用索引签名定义的一个对象类型,注意其中index是自己自定义的,代表属性名的占位,对于对象来说index的类型一般为string(因为对象的key值本身是string类型的,但也有例外的情况,往下看就知道了)

  • 最后的string就代表属性的值的类型了,从这我们不难发现使用索引签名的前提是你知道值的类型。

这时细心的朋友应该能够发现,当index的类型为number时,就能表示数组了,毕竟数组实质上就是一种对象,只不过它的key其实就是数组的索引是number类型的:

interface IObj 
    [index: number]: string;

const arr: IObj = [];
const arr1: IObj = ["Ailjx"];
const obj: IObj = ; // 赋值空对象也不会报错
const obj1: IObj =  1: "1" ; // 赋值key为数字的对象也不会报错
  • index: number时不仅能够表示数组,也能够表示上面所示的两种对象,这就是上面提到的例外的情况。

索引签名的属性类型必须是 stringnumber ,称之为数字索引器字符串索引器,支持两种类型的索引器是可能的,但是从数字索引器返回的类型必须是字符串索引器返回的类型的子类型(这一点==特别重要!==),如:

interface Animal 
    name: string;

interface Dog extends Animal 
    breed: string;


interface IObj 
    [index: number]: Dog;
    [index: string]: Animal;
  • 在[【TypeScript】TypeScript常用类型(下篇)]中我们已经讲解过接口类型的扩展,这里就不再多说了,从上面的代码中可以知道的是DogAnimal 的子类,所以上述代码是可选的,如果换一下顺序就不行了:

字符串索引签名强制要求所有的属性与它的返回类型相匹配

  • 在下面的例子中,name 的类型与字符串索引的类型不匹配,类型检查器会给出一个错误:

然而,如果索引签名是属性类型的联合,不同类型的属性是可以接受的:

interface IObj 
    [index: string]: number | string;
    length: number; // ok
    name: string; // ok

索引签名也可以设置为只读:

2、扩展类型

在[【TypeScript】TypeScript常用类型(下篇)]接口中我们简单介绍过扩展类型,在这里再详细讲一下:

interface User 
    name: string;
    age: number;

interface Admin 
    isAdmin: true;
    name: string;
    age: number;

这里声明了两个类型接口,但仔细发现它们其实是相关的(AdminUser的一种),并且它们之间重复了一些属性,这时就可以使用extends扩展:

interface User 
    name: string;
    age: number;

interface Admin extends User 
    isAdmin: true;

接口上的 extends 关键字,允许我们有效地从其他命名的类型中复制成员,并添加我们想要的任何新成员。

这对于减少我们不得不写的类型声明模板,以及表明同一属性的几个不同声明可能是相关的意图来说,是非常有用的。例如, Admin不需要重复 nameage属性,而且因为 nameage源于User,我们会知道这两种类型在某种程度上是相关的。

接口也可以从多个类型中扩展:

interface User 
    name: string;

interface Age 
    age: number;

interface Admin extends User, Age 
    isAdmin: true;

多个父类使用,分割

3、交叉类型

在[【TypeScript】TypeScript常用类型(下篇)]类型别名中我们已经介绍过交叉类型&,这里就不再过多的说了:

interface Colorful 
    color: string;

interface Circle 
    radius: number;

type ColorfulCircle = Colorful & Circle;

const cc: ColorfulCircle = 
    color: "red",
    radius: 42,
;

4、泛型对象类型

如果我们有一个盒子类型,它的内容可以为字符串,数字,布尔值,数组,对象等等等等,那我们去定义它呢?这样吗:

interface Box 
    contents: any;

现在,内容属性的类型是任意,这很有效,但我们知道any会导致TypeScript失去编译时的类型检查,这显然是不妥的

我们可以使用 unknown ,但这意味着在我们已经知道内容类型的情况下,我们需要做预防性检查,或者使用容易出错的类型断言

interface Box 
    contents: unknown;

let x: Box = 
    contents: "hello world",
;
// 我们需要检查 x.contents
if (typeof x.contents === "string") 
    console.log(x.contents.toLowerCase());

// 或者用类型断言
console.log((x.contents as string).toLowerCase());

这显得复杂了一些,并且也不能保证TypeScript能够追踪到contents具体的类型

针对这种需求,我们就可以使用泛型对象类型,做一个通用的 Box 类型,声明一个类型参数:

// 这里的Type是自定义的
interface Box<Type>  
    contents: Type;

当我们引用 Box 时,我们必须给一个类型参数来代替 Type

const str: Box<string> = 
    contents: "999", // contents类型为string
; // str类型等价于 contents:string 
const str1: Box<number> = 
    contents: 1, // contents类型为number
; // str1类型等价于 contents:number 

这像不像是函数传参的形式?其实我们完全可以将Type理解为形参,在使用类型时通过泛型语法&lt;&gt;传入实参即可

这样我们不就实现了我们想要的效果了吗,contents的类型可以是我们指定的任意的类型,并且TypeScript可以追踪到它具体的类型。

  • 复杂一点的应用:使用泛型对象类型实现通用函数
interface Box<Type> 
    contents: Type;

function setContents<FnType>(box: Box<FnType>, newContents: FnType): FnType 
    box.contents = newContents;
    return box.contents;


const a: string = setContents<string>( contents: "Ailjx" , "9");
console.log(a); // 9
const b: number = setContents( contents: 2 , 2);
console.log(b); // 2
const c: boolean = setContents( contents: true , false);
console.log(c); // false

这里在函数身上使用了泛型,定义了类型参数FnTypesetContents&lt;FnType&gt;,之后函数的参数box的类型为Box&lt;FnType&gt;(将接收到的参数传递给Box),newContents的类型为FnType,函数返回值也是FnType类型

观察常量a,它调用setContents函数时传入了stringstring就会替换掉setContents函数中的所有FnType,则函数的两个参数的类型就是conents:stringstring,函数返回值也是string类型

其实这里调用setContents函数时我们可以不去手动传递类型参数,TypeScript会非常聪明的根据我们调用函数传入的参数类型推断出FnType是什么,就像常量bc的使用一样

类型别名结合泛型

类型别名也可以是通用的,我们完全可以使用类型别名重新定义 Box&lt;Type&gt;

type Box<Type> = 
    contents: Type;
;

由于类型别名与接口不同,它不仅可以描述对象类型,我们还可以用它来编写其他类型的通用辅助类型:

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

上面的例子中嵌套使用了类型别名,多思考一下不难看懂的

5、数组类型

和上面的 Box 类型一样, Array 本身也是一个通用类型, number[]string[] 这实际上只是 Array&lt;number&gt;Array&lt;string&gt; 的缩写。

Array泛型对象的部分源码:

interface Array<Type> 
    /**
     * 获取或设置数组的长度。
     */
    length: number;
    /**
     * 移除数组中的最后一个元素并返回。
     */
    pop(): Type | undefined;
    /**
     * 向一个数组添加新元素,并返回数组的新长度。
     */
    push(...items: Type[]): number;

    // ...

6、只读数组类型

ReadonlyArray 是一个特殊的类型,描述了不应该被改变的数组。

function doStuff(values: ReadonlyArray<string>) 
    // 我们可以从 values 读数据...
    const copy = values.slice();
    console.log(`第一个值是 $values[0]`);
    // ...但我们不能改变 vulues 的值。
    values.push("hello!");
    values[0] = "999";

  • ReadonlyArray&lt;string&gt;与普通数组一样也能够简写,可简写为:readonly string[]

  • 普通的 Array 可以分配给 ReadonlyArray

    const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

    ReadonlyArray 不能分配给普通 Array

7、元组类型

Tuple 类型是另一种 Array 类型,它确切地知道包含多少个元素,以及它在特定位置包含哪些类型。

type MyType = [number, string];
const arr: MyType = [1, "1"]; 

这里的MyType就是一个元组类型,对于类型系统来说, MyType描述了其索
引 0 包含数字和 索引1 包含字符串的数组,当类型不匹配时就会抛出错误:

当我们试图索引超过元素的数量,我们会得到一个错误:

需要注意的是

  • 这里我们虽然只声明了数组的前两个元素的类型,但这不代表数组内只能有两个元素
  • 我们依旧可以向其push新元素,但新元素的类型必须是我们声明过的类型之一
  • 并且添加新元素后虽然数组的长度变化了,但我们依旧无法通过索引访问新加入的元素(能访问到的索引依旧不超过先前类型定义时的元素数量)

    const arr: MyType = [1, "1"];
    arr.push(3);
    arr.push("3");
    console.log(arr, arr.length); // [ 1, 1, 3, 3 ]  4
    console.log(arr[0], arr[1]); // 1 1
    // console.log(arr[2]); // err:长度为 "2" 的元组类型 "MyType" 在索引 "2" 处没有元素。
    // arr.push(true); // err:类型“boolean”的参数不能赋给类型“string | number”的参数

    对元组进行解构:

function fn(a: [string, number]) 
    const [str, num] = a;
    console.log(str); // type str=string
    console.log(num); // type num=number
  • 这里需要注意的是我们解构出的数据是一个常数,不能被修改:

    function fn(a: [string, number]) 
        const [str, num] = a;
        console.log(a[1]++); // ok
        console.log(num++); // err:无法分配到 "num" ,因为它是常数
    

    可选的元组

    元组可以通过在元素的类型后面加上使其变成可选的,它只能出现在数组末尾,而且还能影响到数组长度。

type MyArr = [number, number, number?];
function getLength(arr: MyArr) 
    const [x, y, z] = arr; // z的类型为number|undefined
    console.log(`数组长度:$arr.length `);

getLength([3, 4]); //数组长度:2
getLength([3, 4, 5]); // 数组长度:3
getLength([3, 4, "5"]); // err:不能将类型“string”分配给类型“number”。

其余元素

元组也可以有其余元素,这些元素必须是 array/tuple 类型:

type Arr1 = [string, number, ...boolean[]];
type Arr2 = [string, ...boolean[], number];
type Arr3 = [...boolean[], string, number];
const a: Arr1 = ["Ailjx", 3, true, false, true, false, true];
  • Arr1描述了一个元组,其前两个元素分别是字符串和数字,但后面可以有任意数量的布尔。
  • Arr2描述了一个元组,其第一个元素是字符串,然后是任意数量的布尔运算,最后是一个数字。
  • Arr3描述了一个元组,其起始元素是任意数量的布尔运算,最后是一个字符串,然后是一个数字。

应用

function fn(...args: [string, number, ...boolean[]]) 
    const [name, version, ...input] = args;
    console.log(name, version, input); // 1 1 [ true, false ]
    console.log(参数数量:,args.length); // 参数数量:4
    // ...

fn("1", 1, true, false);

几乎等同于:

function fn(name: string, version: number, ...input: boolean[]) 
   console.log(name, version, input); // 1 1 [ true, false ]
   console.log(input.length + 2); // 参数数量:4
    // ...

fn("1", 1, true, false);

8、只读元组类型

tuple 类型有只读特性,可以通过在它们前面加上一个readonly修饰符来指定:

let arr: readonly [string, number] = ["1", 1];
arr[0] = "9"; // err:无法分配到 "0" ,因为它是只读属性。

在大多数代码中,元组往往被创建并不被修改,所以在可能的情况下,将类型注释为只读元组是一个很好的默认。

带有 const 断言的数组字面量将被推断为只读元组类型,且元素的类型为文字类型:


与只读数组类型中一样,普通的元组可以赋值给只读的元组,但反过来不行:

let readonlyArr: readonly [number, number];
let arr1: [number, number] = [5, 5];
readonlyArr = arr1; // ok
let arr2: [number, number] = readonlyArr; // err

以上是关于来做操吧!深入 TypeScript 高级类型和类型体操的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript高级类型入门手册:附大量代码实例(收藏!)

TypeScript实例_手动编译与自动编译类型注解接口和类的详解

前端进阶-TypeScript高级类型 | 泛型约束泛型接口泛型工具类型

初学typescript

TypeScript学习笔记——TS类型/高级用法及实战优缺点

TypeScript学习笔记——TS类型/高级用法及实战优缺点