0


【TypeScript】深入学习TypeScript对象类型

目录

前言

博主最近的

TypeScript

文章都在TypeScript专栏里,每一篇都是博主精心打磨的精品,几乎每篇文章的质量分都达到了99,并多次入选【CSDN每天值得看】和【全站综合热榜】

订阅专栏关注博主,学习

TypeScript

不迷路!

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

在【TypeScript】TypeScript常用类型(上篇)中我们已经提到了对象类型,这篇文章将对其进行深入讲解

1、属性修改器

可选属性

在【TypeScript】TypeScript常用类型(上篇)我们已经提到了对象的可选属性,在这里我们再深入去了解一下它:

interfacePaintOptions{
    x?:number;
    y?:number;}

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

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

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

functionObjFn({ x =1, y =2}: PaintOptions){console.log(x + y);}ObjFn({ x:4, y:5});// log: 9ObjFn({});// log: 3

在这里,我们为

ObjFn

的参数使用了一个解构模式,并为

x

y

提供了默认值。现在

x

y

都肯定存在于

ObjFn

的主体中,但对于

ObjFn

的任何调用者来说是可选的。

只读属性

TypeScript

中使用

readonly

修饰符可以定义只读属性:

interfaceNameType{readonly name:string;// 只读属性}functiongetName(obj: NameType){// 可以读取 'obj.name'.console.log(obj.name);// 但不能重新设置值
    obj.name ="Ailjx";}

在这里插入图片描述

readonly

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

interfaceInfo{readonly friend:string[];readonly parent:{ father:string; mother:string};}functiongetInfo(obj: Info){// 正常运行
    obj.friend[0]="one";
    obj.parent.father ="MyFather";// 报错
    obj.friend =["one"];
    obj.parent ={ father:"MyFather", mother:"MyMother"};}

在这里插入图片描述

TypeScript

在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是
否是

readonly

,所以

readony

属性也可以通过别名来改变:

interfacePerson{
    name:string;
    age:number;}interfaceReadonlyPerson{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中的只读属性就成功被修改了

对于

TypeScript

而言,只读属性不会在运行时改变任何行为,但在类型检查期间,一个标记为只读的属性不能被写入。

索引签名

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

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

interfaceIObj{[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

类型的:

interfaceIObj{[index:number]:string;}const arr: IObj =[];const arr1: IObj =["Ailjx"];const obj: IObj ={};// 赋值空对象也不会报错const obj1: IObj ={1:"1"};// 赋值key为数字的对象也不会报错
  • index: number时不仅能够表示数组,也能够表示上面所示的两种对象,这就是上面提到的例外的情况。> 这是因为当用 "数字 “进行索引时,> > JavaScript> > 实际上会在索引到一个对象之前将其转换为 “字符串”。这意味着用1 (一个数字)进行索引和用"1” (一个字符串)进行索引是一样的,所以两者需要一致。

索引签名的属性类型必须是

string

number

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

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

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

  • 在下面的例子中,name 的类型与字符串索引的类型不匹配,类型检查器会给出一个错误:在这里插入图片描述> 数字索引签名没有该限制

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

interfaceIObj{[index:string]:number|string;
    length:number;// ok
    name:string;// ok}

索引签名也可以设置为只读:
在这里插入图片描述

2、扩展类型

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

interfaceUser{
    name:string;
    age:number;}interfaceAdmin{
    isAdmin:true;
    name:string;
    age:number;}

这里声明了两个类型接口,但仔细发现它们其实是相关的(

Admin

User

的一种),并且它们之间重复了一些属性,这时就可以使用

extends

扩展:

interfaceUser{
    name:string;
    age:number;}interfaceAdminextendsUser{
    isAdmin:true;}

接口上的

extends

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

这对于减少我们不得不写的类型声明模板,以及表明同一属性的几个不同声明可能是相关的意图来说,是非常有用的。例如,

Admin

不需要重复

name

age

属性,而且因为

name

age

源于

User

,我们会知道这两种类型在某种程度上是相关的。

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

interfaceUser{
    name:string;}interfaceAge{
    age:number;}interfaceAdminextendsUser, Age {
    isAdmin:true;}

多个父类使用

,

分割

3、交叉类型

在【TypeScript】TypeScript常用类型(下篇)类型别名中我们已经介绍过交叉类型

&

,这里就不再过多的说了:

interfaceColorful{
    color:string;}interfaceCircle{
    radius:number;}typeColorfulCircle= Colorful & Circle;const cc: ColorfulCircle ={
    color:"red",
    radius:42,};

4、泛型对象类型

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

interfaceBox{
    contents:any;}

现在,内容属性的类型是任意,这很有效,但我们知道

any

会导致

TypeScript

失去编译时的类型检查,这显然是不妥的

我们可以使用

unknown

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

interfaceBox{
    contents:unknown;}let x: Box ={
    contents:"hello world",};// 我们需要检查 'x.contents'if(typeof x.contents ==="string"){console.log(x.contents.toLowerCase());}// 或者用类型断言console.log((x.contents asstring).toLowerCase());

这显得复杂了一些,并且也不能保证

TypeScript

能够追踪到

contents

具体的类型

针对这种需求,我们就可以使用泛型对象类型,做一个通用的

Box

类型,声明一个类型参数:

// 这里的Type是自定义的interfaceBox<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

理解为形参,在使用类型时通过泛型语法

<>

传入实参即可

这样我们不就实现了我们想要的效果了吗,

contents

的类型可以是我们指定的任意的类型,并且

TypeScript

可以追踪到它具体的类型。

  • 复杂一点的应用:使用泛型对象类型实现通用函数
interfaceBox<Type>{
    contents: Type;}functionsetContents<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);// 2const c:boolean=setContents({ contents:true},false);console.log(c);// false

这里在函数身上使用了泛型,定义了类型参数

FnType

setContents<FnType>

,之后函数的参数

box

的类型为

Box<FnType>

(将接收到的参数传递给

Box

),

newContents

的类型为

FnType

,函数返回值也是

FnType

类型

观察常量

a

,它调用

setContents

函数时传入了

string

string

就会替换掉

setContents

函数中的所有

FnType

,则函数的两个参数的类型就是

{conents:string}

string

,函数返回值也是

string

类型

其实这里调用

setContents

函数时我们可以不去手动传递类型参数,

TypeScript

会非常聪明的根据我们调用函数传入的参数类型推断出

FnType

是什么,就像常量

b

c

的使用一样

类型别名结合泛型

类型别名也可以是通用的,我们完全可以使用类型别名重新定义

Box<Type>

typeBox<Type>={
    contents: Type;};

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

typeOrNull<Type>= Type |null;typeOneOrMany<Type>= Type | Type[];typeOneOrManyOrNull<Type>= OrNull<OneOrMany<Type>>;typeOneOrManyOrNullStrings= OneOrManyOrNull<string>;

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

通用对象类型通常是某种容器类型,它的工作与它们所包含的元素类型无关。数据结构以这种方式工作是很理想的,这样它们就可以在不同的数据类型中重复使用。

5、数组类型

和上面的

Box

类型一样,

Array

本身也是一个通用类型,

number[]

string[] 这

实际上只是

Array<number>

Array<string>

的缩写。

Array

泛型对象的部分源码:

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

现代

JavaScript

还提供了其他通用的数据结构,比如

Map<K, V>

,

Set<T>

, 和

Promise<T>

。这实际上意味着,由于

Map

Set

Promise

的行为方式,它们可以与任何类型的集合一起工作。

6、只读数组类型

ReadonlyArray

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

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

在这里插入图片描述

  • ReadonlyArray<string>与普通数组一样也能够简写,可简写为:readonly string[]
  • 普通的 Array 可以分配给 ReadonlyArrayconst roArray: ReadonlyArray<string>=["red","green","blue"];ReadonlyArray 不能分配给普通 Array在这里插入图片描述

7、元组类型

Tuple

类型是另一种

Array

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

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

对元组进行解构:

functionfn(a:[string,number]){const[str, num]= a;console.log(str);// type str=stringconsole.log(num);// type num=number}
  • 这里需要注意的是我们解构出的数据是一个常数,不能被修改:functionfn(a:[string,number]){const[str, num]= a;console.log(a[1]++);// okconsole.log(num++);// err:无法分配到 "num" ,因为它是常数}

可选的元组

元组可以通过在元素的类型后面加上

使其变成可选的,它只能出现在数组末尾,而且还能影响到数组长度。

typeMyArr=[number,number,number?];functiongetLength(arr: MyArr){const[x, y, z]= arr;// z的类型为number|undefinedconsole.log(`数组长度:${arr.length}`);}getLength([3,4]);//数组长度:2getLength([3,4,5]);// 数组长度:3getLength([3,4,"5"]);// err:不能将类型“string”分配给类型“number”。

其余元素

元组也可以有其余元素,这些元素必须是

array/tuple

类型:

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

应用

functionfn(...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);

几乎等同于:

functionfn(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;// oklet arr2:[number,number]= readonlyArr;// err

结语

上篇文章【TypeScript】TypeScript中类型缩小(含类型保护)与类型谓词入选了《全站综合热榜》,感谢CSDN和各位支持我的朋友,我会继续努力,保持状态创作出更多更好的文章!


本文转载自: https://blog.csdn.net/m0_51969330/article/details/126012732
版权归原作者 海底烧烤店ai 所有, 如有侵权,请联系我们删除。

“【TypeScript】深入学习TypeScript对象类型”的评论:

还没有评论