ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

[TypeScript学习笔记-06]如何定义对象类型

[TypeScript学习笔记-06]如何定义对象类型

在JavaScript中,我们组织和传递数据的基本方式是通过对象。在TypeScript中,我们通过对象类型来表示这些对象。

1. 对象的三种定义方式

1.1 接口

interfacePerson{name:string;age:number;}functiongreet(person:Person){return"Hello "+person.name;}

1.2 类型别名

typePerson={name:string;age:number;};functiongreet(person:Person){return"Hello "+person.name;}

1.3 匿名类型

functiongreet(person:{name:string;age:number}){return"Hello "+person.name;}

2. 属性修饰符

2.1 可缺省

interfacePaintOptions{shape:Shape;xPos?:number;yPos?:number;}functionpaintShape(opts:PaintOptions){letxPos=opts.xPos;//(property) PaintOptions.xPos?: number | undefinedletyPos=opts.yPos;//(property) PaintOptions.yPos?: number | undefined}

-undefined检验

functionpaintShape(opts:PaintOptions){letxPos=opts.xPos===undefined?0:opts.xPos;// let xPos: numberletyPos=opts.yPos===undefined?0:opts.yPos;// let yPos: number// ...}
  • 对象解构 + 默认值
functionpaintShape({shape,xPos=0,yPos=0}:PaintOptions){console.log("x coordinate at",xPos);//(parameter) xPos: numberconsole.log("y coordinate at",yPos);//(parameter) yPos: number}
  • 使用解构模式时不能标准类型
// 不合法// 在对象解构模式中,shape: Shape 表示*获取属性 shape 并将其重新定义为名为 Shape 的局部变量*。functiondraw({shape:Shape,xPos:number=100/*...*/}){}

2.2 只读属性

interfaceSomeType{readonlyprop:string;}functiondoSomething(obj:SomeType){console.log(`prop has the value '${obj.prop}'.`);// OKobj.prop="hello";// Fail}
  • 使用readonly修饰符并不一定意味着一个值是完全不可更改的,只是当前属性成员不可替换:
interfaceHome{readonlyresident:{name:string;age:number};}functionvisitForBirthday(home:Home){console.log(`Happy birthday${home.resident.name}!`);home.resident.age++;// OK}
  • TypeScript在检查两种类型是否兼容时,不会考虑两种类型的属性是否为只读,因此只读属性也可以通过别名进行更改:
interfacePerson{name:string;age:number;}interfaceReadonlyPerson{readonlyname:string;readonlyage:number;}letwritablePerson:Person={name:"Person McPersonface",age:42,};// OKletreadonlyPerson:ReadonlyPerson=writablePerson;console.log(readonlyPerson.age);// prints '42'writablePerson.age++;console.log(readonlyPerson.age);// prints '43'

2.3 索引签名

有时你无法预先知道某个类型所有属性的名称,但你知道其值的形式。在这种情况下,你可以使用索引签名来描述可能的值的类型,例如:

interfaceStringArray{[index:number]:string;}constmyArray:StringArray=getStringArray();constsecondItem=myArray[1];// const secondItem: string
  • 可以支持多种类型的索引器。当同时使用number和string索引器时,数字索引器返回的类型必须是字符串索引器返回类型的子类型。
interfaceAnimal{name:string;}interfaceDogextendsAnimal{breed:string;}// 正确interfaceOkay{[x:number]:Dog;[x:string]:Animal;}// 错误interfaceNotOkay{[x:number]:Animal;[x:string]:Dog;}

注: 这是因为当使用数字进行索引时,JavaScript实际上会先将数字转换为字符串,然后再对对象进行索引。这意味着使用数字100进行索引与使用字符串"100"进行索引的效果相同,因此两者必须保持一致。

  • 虽然字符串索引签名是描述字典模式的一种强大方式,但它们也强制所有属性与其返回类型匹配。
interfaceNumberDictionary{[index:string]:number;length:number;// 正确name:string;// 错误}

注: 这是因为字符串索引声明obj.property也可以通过obj["property"]访问。

  • 如果索引签名是属性类型的并集,则允许存在不同类型的属性:
interfaceNumberOrStringDictionary{[index:string]:number|string;length:number;// 正确name:string;// 正确}
  • 可以将索引签名设为只读,以防止对索引进行赋值:
interfaceReadonlyStringArray{readonly[index:number]:string;}letmyArray:ReadonlyStringArray=getReadOnlyStringArray();myArray[2]="Mallory";// 错误

3. 额外属性检查

对象被赋予类型的位置和方式会对类型系统产生影响。一个关键的例子是额外属性检查,它会在创建对象并将其分配给对象类型时,对对象进行更彻底的验证。

interfaceSquareConfig{color?:string;width?:number;}functioncreateSquare(config:SquareConfig):{color:string;area:number}{return{color:config.color||"red",area:config.width?config.width*config.width:20,};}letmySquare=createSquare({colour:"red",width:100});// Object literal may only specify known properties,// but 'colour' does not exist in type 'SquareConfig'.// Did you mean to write 'color'?
  • 可以使用基于as的类型转换解决问题:
letmySquare=createSquare({width:100,opacity:0.5}asSquareConfig);
  • 如果您确定对象可以具有一些以特殊方式使用的额外属性,那么更好的方法可能是添加字符串索引签名。
interfaceSquareConfig{color?:string;width?:number;[propName:string]:unknown;}
  • 另一种解决方案是将对象赋值给另一个变量:由于赋值squareOptions不会进行额外的属性检查,因此编译器不会报错:
letsquareOptions={colour:"red",width:100};letmySquare=createSquare(squareOptions);

注:TypeScript在对象字面量赋值时有一个特殊的规则:额外属性检查只在直接传字面量时触发。

  • 只要squareOptions和SquareConfig之间存在共同属性,上述解决方法就能奏效。否则此方法会失败。例如:
letsquareOptions={colour:"red"};letmySquare=createSquare(squareOptions);// Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.

4. 扩展类型

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

interfaceBasicAddress{name?:string;street:string;city:string;country:string;postalCode:string;}interfaceAddressWithUnitextendsBasicAddress{unit:string;}
  • 接口也可以从多种类型扩展而来
interfaceColorful{color:string;}interfaceCircle{radius:number;}interfaceColorfulCircleextendsColorful,Circle{}constcc:ColorfulCircle={color:"red",radius:42,};

5 交集类型

接口允许我们通过扩展其他类型来构建新的类型。TypeScript还提供了另一种称为交集类型的结构,主要用于组合现有的对象类型。

interfaceColorful{color:string;}interfaceCircle{radius:number;}typeColorfulCircle=Colorful&Circle;

注:可以从字面意义理解为交集,即同时拥各个集合(类型)的成员。

functiondraw(circle:Colorful&Circle){console.log(`Color was${circle.color}`);console.log(`Radius was${circle.radius}`);}// 正确draw({color:"blue",radius:42});// 错误draw({color:"red",raidus:42});

6. 接口扩展和交集类型

对于接口,我们可以使用extends子句来继承其他类型;对于交集类型,我们也可以做类似的事情,并用类型别名来命名结果。两者之间的主要区别在于冲突的处理方式,而这种区别通常也是在接口和交集类型的类型别名之间进行选择的主要原因之一。

  • 如果定义了同名接口,TypeScript会在属性兼容的情况下尝试合并它们。如果属性不兼容(如下所示,属性名相同但类型不同),TypeScript会抛出错误。
interfacePerson{name:string;}interfacePerson{name:number;}
  • 对于交集类型,不同类型的属性会自动合并。之后使用该类型时,TypeScript会期望该属性同时满足两种类型,这可能会导致意想不到的结果。
interfacePerson1{name:string;}interfacePerson2{name:number;}typeStaff=Person1&Person2declareconststaffer:Staff;staffer.name;//(property) name: never

7. 泛型对象类型

可以将 泛型对象类型 视为真实类型的模板,其中 泛型参数 是一个占位符,将被替换为其他类型。

interfaceBox<Type>{contents:Type;}

7.1Array<Type>

当我们写出像number[]或string[]这样的类型时,实际上只是Array<number>和Array<string>的简写形式。

functiondoSomething(value:Array<string>){// ...}letmyArray:string[]=["hello","world"];doSomething(myArray);doSomething(newArray("hello","world"));
  • Array类型定义
interfaceArray<Type>{length:number;pop():Type|undefined;push(...items:Type[]):number;...}

7.2ReadonlyArray<Type>

ReadonlyArray是一种特殊类型,用于描述不应该被更改的数组。

functiondoStuff(values:ReadonlyArray<string>){values.push("hello!");// 错误}
  • 没有可用的ReadonlyArray构造函数。
// 错误newReadonlyArray("red","green","blue");// 正确constarray:ReadonlyArray<string>=["red","green","blue"];
  • 快捷使用方式
functiondoStuff(values:readonlystring[]){values.push("hello!");// 错误}
  • 与只读属性修饰符不同,普通数组和只读数组之间的赋值不是双向的。
letx:readonlystring[]=[];lety:string[]=[];x=y;// 正确y=x;// 错误

7.3 元组类型

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

typeStringNumberPair=[string,number];functiondoSomething(pair:[string,number]){consta=pair[0];//const a: stringconstb=pair[1];//const b: number// ...}doSomething(["hello",42]);
  • 如果我们尝试索引超过元素个数,就会出错。
functiondoSomething(pair:[string,number]){constc=pair[2];// 错误}
  • 我们还可以使用JavaScript的数组解构来解构元组:
functiondoSomething(stringHash:[string,number]){const[inputString,hash]=stringHash;console.log(inputString);//const inputString: stringconsole.log(hash);//const hash: number}
  • 除了长度检查之外,像这样的简单元组类型等同于数组的版本,这些数组声明特定索引的属性,并使用数值字面量类型声明长度。
interfaceStringNumberPair{length:2;0:string;1:number;slice(start?:number,end?:number):Array<string|number>;}
  • 元组可以通过在元素类型后添加问号(?)来添加可选属性。可选元组元素只能位于末尾,并且还会影响元组的长度类型。
typeEither2dOr3d=[number,number,number?];functionsetCoordinate(coord:Either2dOr3d){const[x,y,z]=coord;//const z: number | undefinedconsole.log(`Provided coordinates had${coord.length}dimensions`);//(property) length: 2 | 3}

注: 长度为字面量类型的联合

  • 元组还可以有剩余元素,这些元素必须是数组/元组类型。
typeStringNumberBooleans=[string,number,...boolean[]];typeStringBooleansNumber=[string,...boolean[],number];typeBooleansStringNumber=[...boolean[],string,number];consta:StringNumberBooleans=["hello",1];constb:StringNumberBooleans=["beautiful",2,true];constc:StringNumberBooleans=["world",3,true,false,true,false,true];
  • 如下两种方式等效:
functionreadButtonInput(...args:[string,number,...boolean[]]){const[name,version,...input]=args;// ...}functionreadButtonInput(name:string,version:number,...input:boolean[]){// ...}

7.4 只读元组

元组类型有只读变体,可以通过在前面加上readonly修饰符来指定,就像数组简写语法一样。

functiondoSomething(pair:readonly[string,number]){// ...}functiondoSomething(pair:readonly[string,number]){pair[0]="hello!";// 错误}
  • 带有const断言的数组字面量会被推断为只读元组类型。
letpoint=[3,4]asconst;functiondistanceFromOrigin([x,y]:[number,number]){returnMath.sqrt(x**2+y**2);}distanceFromOrigin(point);// 错误: 不能将自读元组赋值给常规元组
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进