政务服务平台开发需要注意如何提升小程序跨平台兼容性与用户体验
848
2022-10-12
【TypeScript】深入学习TypeScript对象类型
目录
前言
1、属性修改器
可选属性只读属性索引签名
2、扩展类型3、交叉类型4、泛型对象类型
类型别名结合泛型
5、数组类型6、只读数组类型7、元组类型
可选的元组其余元素应用
8、只读元组类型
结语
前言
博主最近的TypeScript文章都在TypeScript专栏里,每一篇都是博主精心打磨的精品,几乎每篇文章的质量分都达到了99,并多次入选【CSDN每天值得看】和【全站综合热榜】
订阅专栏关注博主,学习TypeScript不迷路!
好嘞,言归正传,让我们开始深入学习TypeScript对象类型吧:
在【TypeScript】TypeScript常用类型(上篇)中我们已经提到了对象类型,这篇文章将对其进行深入讲解
1、属性修改器
可选属性
在【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: 9ObjFn({}); // log: 3
在这里,我们为 ObjFn的参数使用了一个解构模式,并为 x和 y提供了默认值。现在x和 y 都肯定存在于 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'
这里有点绕,我们来梳理一下:
对于TypeScript而言,只读属性不会在运行时改变任何行为,但在类型检查期间,一个标记为只读的属性不能被写入。
索引签名
在一些情况下,我们可能不知道对象内所有属性的名称,那属性名称都不知道,我们该怎么去定义这个对象的类型呢?
这时我们可以使用一个索引签名来描述可能的值的类型:
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时不仅能够表示数组,也能够表示上面所示的两种对象,这就是上面提到的例外的情况。
这是因为当用 "数字 “进行索引时,JavaScript实际上会在索引到一个对象之前将其转换为 “字符串”。这意味着用1 (一个数字)进行索引和用"1” (一个字符串)进行索引是一样的,所以两者需要一致。
索引签名的属性类型必须是 string 或 number ,称之为数字索引器和字符串索引器,支持两种类型的索引器是可能的,但是从数字索引器返回的类型必须是字符串索引器返回的类型的子类型(这一点特别重要!),如:
interface Animal { name: string;}interface Dog extends Animal { breed: string;}interface IObj { [index: number]: Dog; [index: string]: Animal;}
字符串索引签名强制要求所有的属性与它的返回类型相匹配。
数字索引签名没有该限制
然而,如果索引签名是属性类型的联合,不同类型的属性是可以接受的:
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;}
interface User { name: string; age: number;}interface Admin extends User { isAdmin: true;}
接口也可以从多个类型中扩展:
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具体的类型
// 这里的Type是自定义的interface Box
当我们引用 Box 时,我们必须给一个类型参数来代替 Type:
const str: Box
这像不像是函数传参的形式?其实我们完全可以将Type理解为形参,在使用类型时通过泛型语法<>传入实参即可
这样我们不就实现了我们想要的效果了吗,contents的类型可以是我们指定的任意的类型,并且TypeScript可以追踪到它具体的类型。
复杂一点的应用:使用泛型对象类型实现通用函数
interface Box
这里在函数身上使用了泛型,定义了类型参数FnType :setContents
观察常量a,它调用setContents函数时传入了string,string就会替换掉setContents函数中的所有FnType,则函数的两个参数的类型就是{conents:string}和string,函数返回值也是string类型
其实这里调用setContents函数时我们可以不去手动传递类型参数,TypeScript会非常聪明的根据我们调用函数传入的参数类型推断出FnType是什么,就像常量b和c的使用一样
类型别名结合泛型
类型别名也可以是通用的,我们完全可以使用类型别名重新定义 Box
type Box
由于类型别名与接口不同,它不仅可以描述对象类型,我们还可以用它来编写其他类型的通用辅助类型:
type OrNull
上面的例子中嵌套使用了类型别名,多思考一下不难看懂的
通用对象类型通常是某种容器类型,它的工作与它们所包含的元素类型无关。数据结构以这种方式工作是很理想的,这样它们就可以在不同的数据类型中重复使用。
5、数组类型
和上面的 Box 类型一样, Array 本身也是一个通用类型, number[] 或 string[] 这实际上只是 Array
Array泛型对象的部分源码:
interface Array
现代JavaScript还提供了其他通用的数据结构,比如 Map
6、只读数组类型
ReadonlyArray 是一个特殊的类型,描述了不应该被改变的数组。
function doStuff(values: ReadonlyArray
ReadonlyArray
const roArray: ReadonlyArray
而 ReadonlyArray 不能分配给普通 Array :
7、元组类型
Tuple 类型是另一种 Array 类型,它确切地知道包含多少个元素,以及它在特定位置包含哪些类型。
type MyType = [number, string];const arr: MyType = [1, "1"];
这里的MyType就是一个元组类型,对于类型系统来说, MyType描述了其索 引 0 包含数字和 索引1 包含字符串的数组,当类型不匹配时就会抛出错误:
当我们试图索引超过元素的数量,我们会得到一个错误:
需要注意的是:
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”的参数
对元组进行解构:
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]); //数组长度:2getLength([3, 4, 5]); // 数组长度:3getLength([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; // oklet arr2: [number, number] = readonlyArr; // err
结语
上篇文章【TypeScript】TypeScript中类型缩小(含类型保护)与类型谓词入选了《全站综合热榜》,感谢CSDN和各位支持我的朋友,我会继续努力,保持状态创作出更多更好的文章!
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~