可以为对象类型的参数添加注解以规范对象定义:
function printUser(user: { name: string, age: number }): void {
console.log("姓名:" + user.name + ",年龄:" + user.age);
}
printUser({name:"icexmoon",age:15});
对象类型也可以用类型别名定义:
type Person = { name: string, age: number };
function printUser(user: Person): void {
console.log("姓名:" + user.name + ",年龄:" + user.age);
}
printUser({name:"icexmoon",age:15});
也可以使用interface定义:
interface Person {
name: string,
age: number
}
function printUser(user: Person): void {
console.log("姓名:" + user.name + ",年龄:" + user.age);
}
printUser({ name: "icexmoon", age: 15 });
属性修饰符
TS 中,可以通过属性修饰符定义属性的类型、是否可选以及属性是否可写。
可选属性
可以通过?定义一个属性是可选的:
interface Circle {
radius: number,
positionX?: number,
positionY?: number
}
function printCircle(circle: Circle) {
let positionX = 0;
let positionY = 0;
if (circle.positionX !== undefined) {
positionX = circle.positionX;
}
if (circle.positionY !== undefined) {
positionY = circle.positionY;
}
console.log("圆心(" + positionX + "," + positionY + "),半径:" + circle.radius);
}
printCircle({ radius: 1 });
printCircle({ positionX: 2, radius: 3 })
printCircle({ positionX: 3, positionY: 5, radius: 1 })
和函数的可选参数一样,在这种情况下我们需要在接收对象并进行处理时判断是不是为undefined。这会让代码看上去比较繁琐,可以使用参数结构和默认参数进行简化:
function printCircle2({ radius, positionX = 0, positionY = 0 }: Circle) {
console.log("圆心(" + positionX + "," + positionY + "),半径:" + radius);
}
只读属性
使用readonly可以将一个属性标记为只读属性:
interface Person{
readonly id: number,
name: string,
age: number
}
function handlerPerson(person: Person){
person.name = "icexmoon";
person.age = 19;
person.id = 123; // 报错,无法为“id”赋值,因为它是只读属性
}
和 Java 中的final一样,只读属性只是限制赋值行为,如果只改变其中的属性值,是被允许的:
interface Person2 {
name: string,
age: number
pet: {
kind: "dog" | "cat",
name: string
}
}
function handlePerson2(person: Person2){
person.name = 'LiLei';
person.age = 11;
person.pet.kind = 'cat';
person.pet.name = 'Tom';
}
此外,因为 TS 判断变量是否是某个类型时,采用的是鸭子类型的原则,因此只读属性是不会影响类型匹配的:
interface ReadOnlyPerson {
readonly name: string,
readonly age: number
}
interface WritablePerson {
name: string,
age: number
}
const person1: WritablePerson = { name: "Tom", age: 11 };
const person2: ReadOnlyPerson = person1;
console.log(person2); // { name: 'Tom', age: 11 }
person1.age = 15;
console.log(person2); // { name: 'Tom', age: 15 }
索引签名
在 JavaScript 中,可以使用索引的方式访问对象属性:
const person5 = {
name: 'Tom',
age: 15
}
console.log(person5['name'])
console.log(person5['age'])
TS 可以用索引签名标记属性的索引类型和值类型:
interface Person3 {
[index: string]: string | number;
name: string;
age: number;
}
const person6: Person3 = { name: 'Jack', age: 16 };
索引签名中的索引类型只能是string、number、symbol、String pattern,或者是这些类型的联合。
索引签名可以使用readonly,此时索引值不能再改变:
interface StringArr{
readonly [index: number]: string;
}
function generateStringArr(): StringArr{
const arr = [];
arr[0] = 'hello';
arr[1] = 'world';
return arr;
}
const stringArr = generateStringArr();
stringArr[1] = 'how'; // 报错,类型“StringArr”中的索引签名仅允许读取
多余属性检查
当使用字面量传递对象给函数时,TS 会进行多余属性检查:
interface SquareConfig{
color?: string;
width?: number;
}
function setSquareConfig(config: SquareConfig):void{
console.log(`color:${config.color},width:${config.width}`);
}
setSquareConfig({colour:"red", width: 15}); // 报错,对象字面量只能指定已知的属性
这段代码可以执行,但 TS 检查器会报错。TS 认为这里很可能是代码编写错误(的确如此)。
JavaScript 本身语法是很灵活的,因此这里 TS 并没有严格限制以禁止传递多余的属性,只是提示可能存在问题。如果你的确要传递具有多余属性的字面量对象给函数,可以:
setSquareConfig({colour:"red", width: 15} as SquareConfig);
此外,TS 只会在传递字面量定义的对象时检查,因此另一种绕过这个限制的方式是改为传递变量:
const squareConfig = {colour:"red", width: 15};
setSquareConfig(squareConfig);
最后,如果要频繁的这么做,可能问题在于类型定义,这说明你定义的类型本身需要在已有若干属性的基础上,由客户端程序灵活添加更多的属性。这可以通过索引签名实现:
interface Person7 {
name: string;
age: number;
[index: string]: unknown;
}
function handlePerson(person: Person7){
console.log(person)
}
handlePerson({name:'Tom', age: 15, school: '一中'});
这里的索引签名[index: string]: unknown;表示Person7除了已经定义的两个属性外,还有其他索引为string值类型未知的属性。在这种情况下即使传递的字面量对象包含额外属性,TS 检查器也未报错。
扩展类型
interface定义的类型可以使用extends关键字实现扩展:
interface Person8 {
name: string;
age: number;
}
interface Student extends Person8 {
school: string;
}
function handleStudent(student: Student): void {
console.log(student);
}
handleStudent({ name: 'Tom', age: 15, school: '一中' });
和 Java 中的接口一样,TS 的interface可以实现”多继承“:
interface Circle {
radius: number;
}
interface Colorful {
color: string;
}
interface ColorfulCircle extends Circle, Colorful {
}
function handleColorfulCircle(colorfulCircle: ColorfulCircle){
console.log(colorfulCircle);
}
handleColorfulCircle({radius: 11, color: 'red'});
交叉类型
可以使用&定义一个交叉类型,即同时符合多个类型的类型:
type ColorfulCircle2 = Circle & Colorful;
function handleColorfulCircle2(colorfulCircle: ColorfulCircle2){
console.log(colorfulCircle);
}
handleColorfulCircle2({radius: 11, color: 'red'});
可以看到,这里的交叉类型ColorfulCircle2具备多个类型的所有属性。
除了用交叉类型定义新类型,还可以直接用于类型注解:
function handleColorfulCircle3(colorfulCircle: Colorful & Circle){
console.log(colorfulCircle);
}
接口扩展与交集
TS 中是允许interface重复定义的:
interface Person10{
id: number;
name: string;
}
interface Person10{
id: number;
age: number;
}
function handlePerson10(person: Person10){
console.log(person.id);
console.log(person.name);
console.log(person.age);
}
并且我们看到,当使用interface重复定义一个同名类型时,这个类型最终的定义是将属性合并后的结果。
但这是有限制的,如果合并时同名属性的类型不同:
interface Person11{
id: number;
}
interface Person11{
id: string; // 报错,后续属性声明必须属于同一类型
}
使用&交叉类型时,情况会略有不同,即使多个类型具有同名但不同类型的属性,也可以合并:
interface Pet2 {
id: number;
}
interface Pet3 {
id: string;
}
type Pet5 = Pet2 & Pet3;
declare const pet: Pet5;
pet.id; // 这里 id 推断出的类型是 never
可以看到,同名不同类型的属性合并后,TS 会认为这个属性的类型是never。
泛型对象类型
看一个例子:
interface Box {
content: any
}
这里Box是一个容器类型,属性content可以保存任何类型的值,虽然可以使用,但不能推断出确切类型:
function setContent(box: Box, content: any) {
box.content = content;
}
let box: Box = { content: null };
setContent(box, 123);
box.content.toFixed(2); // 这里的 box.content 类型推断是 any
当然也可以定义不同的类型,并使用函数重载:
interface NumberBox {
content: number
}
interface StringBox {
content: string
}
interface BooleanBox {
content: boolean
}
function setContent3(box: NumberBox, content: number): void;
function setContent3(box: StringBox, content: string): void;
function setContent3(box: BooleanBox, content: boolean): void;
function setContent3(box: { content: any }, content: any): void {
box.content = content;
}
但这样做就太过繁琐,正确的方式是使用泛型:
interface Box2<Type>{
content: Type
}
function setContent2<Type>(box: Box2<Type>, content: Type) {
box.content = content;
}
let box2: Box2<number> = { content: 0 };
setContent2(box2, 123);
box2.content.toFixed(2);
console.log(box2.content)
泛型可以和类型别名结合使用,虽然一般不需要这么做,比如:
interface Apple {
color: string;
weight: number;
}
type AppleBox = Box2<Apple>;
除了使用接口定义泛型对象,还可以使用类型别名,比如:
type Box5<Type> = { content: Type };
和接口相比,这样做的好处是可以使用类型联合:
type OneOrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OneOrMany<Type> | null;
type OneOrManyOrNullOrString = OneOrManyOrNull<string>;
数组类型
泛型对象最常见的用途是定义各种容器,其中数组是最常见的:
function printFirst<Type>(arr:Array<Type>){
console.log(arr[0]);
}
printFirst([1,2,3]);
printFirst(new Array('a','b','c'));
也可以使用Type[]代替Array<Type>,这是一种简写方式:
function printFirst2<Type>(arr:Type[]){
console.log(arr[0]);
}
ReadonlyArray
TS 提供一个特殊的类型ReadonlyArray,表示只读的数组:
function printFirst3<Type>(arr:ReadonlyArray<Type>){
console.log(arr[0]);
arr.push(1); // 报错,类型“readonly Type[]”上不存在属性“push”。
}
同样的,ReadonlyArray也有简写形式:
function printFirst4<Type>(arr:readonly Type[]){
console.log(arr[0]);
arr.push(5); // 错误,类型“readonly Type[]”上不存在属性“push”。
}
元组类型
元组(Tuple)可以看作特殊的数组,它的长度和元素类型都是固定的:
function dealTuple(tuple: [number, string]){
console.log(tuple[0]);
console.log(tuple.length); // 这里的 tuple.length 类型推断是 2
}
dealTuple([1,'2']);
通常会对元组使用解构赋值:
function dealTuple2(tuple: [number, string]){
const [a,b] = tuple;
console.log(a);
console.log(b);
}
dealTuple2([1,'2']);
元组可以定义可选属性:
type TwoOrThreeTuple = [number, number ,number?]
function dealTupele3(tuple: TwoOrThreeTuple){
const [a,b,c] = tuple;
console.log(a);
console.log(b);
console.log(c); // 类型推断是 number|undefined
}
可选属性只能定义在末尾。
元组也可以有剩余元素(rest element):
type NumberStringBooleans = [number, string, boolean[]]
type NumberStringsBoolean = [number, string[], boolean]
type NumbersStringBoolean = [number[], string, boolean]
const tuple1: NumberStringBooleans = [1,'2',true,false];
const tuple2: NumberStringsBoolean = [1,'2','3',true];
const tuple3: NumbersStringBoolean = [1,2,3,'2',true];
用...定义的多余元素只能是数组或元组类型。
利用这种特性,可以这么定义和接收函数参数:
function doSomething(args: [number, string, boolean[]]){
const [arg1,arg2,arg3] = args;
}
和下面的函数定义是等价的:
function doSomething2(arg1:number, arg2:string, arg3:boolean[]){
}
只读元组
function printTuple(tuple: readonly [string, number]): void {
const [a, b] = tuple;
tuple[0] = '1'; // 报错,无法为“0”赋值,因为它是只读属性。
}
通常来说,元组的使用场景都是定义后不再修改内容,因此将元组定义为只读是一个良好的编程习惯。
要注意区分普通的数组字面量和只读元组的区别:
let tuple5 = ['1',2];
// printTuple(tuple5); // 报错,类型(string|number)[] 不能分配给类型 readonly [string, number]
需要修改为:
let tuple6 = ['1',2] as const;
printTuple(tuple6);
The End.
参考资料

文章评论