TypeScript 全面详解:对象类型的语法规则与实战指南

TypeScript 全面详解:对象类型的语法规则与实战指南

🔥全面解析 TypeScript 对象类型的语法细节和使用规范。

一、对象类型的基础声明

1. 直接字面量声明

对象类型最简单的声明方式,就是使用大括号 {} 包裹,内部逐一声明每个属性的名称和对应类型,这是最直观的对象类型定义方式。

typescript 复制代码
// 变量后直接声明对象类型
const obj: {
  x: number;
  y: number;
} = { x: 1, y: 1 };

上述示例中,变量 obj 的类型直接写在变量名后,大括号内明确约束了 xy 两个属性均为 number 类型,赋值时必须严格匹配该类型结构。

2. 属性分隔符规则

对象类型内部的属性之间,支持分号 ;逗号 , 作为分隔符,两种写法效果完全一致,可根据个人或团队编码风格选择。

typescript 复制代码
// 写法 1:分号分隔(推荐,更清晰区分属性与类型结束)
type MyObj1 = {
  x: number;
  y: number;
};

// 写法 2:逗号分隔(贴近 JavaScript 对象字面量写法)
type MyObj2 = {
  x: number,
  y: number,
};

另外,最后一个属性后面的分隔符是可选的,可写可不写,不会影响类型校验。

typescript 复制代码
// 合法:最后一个属性后无分隔符
type MyObj3 = {
  x: number;
  y: number
};

3. 严格的属性匹配规则

一旦声明了对象类型,赋值时就必须严格遵守「不多不少」的原则:不能缺少声明过的属性,也不能额外添加未声明的属性。

typescript 复制代码
type MyObj = {
  x: number;
  y: number;
};

// 错误:缺少属性 y(Property 'y' is missing in type '{ x: number; }')
const o1: MyObj = { x: 1 };

// 错误:多余属性 z(Object literal may only specify known properties)
const o2: MyObj = { x: 1, y: 1, z: 1 };

这种严格校验仅针对「对象字面量直接赋值」,如果是将对象赋值给一个变量后再传递,多余属性不会报错(这是 TS 的「对象字面量新鲜度」规则),但仍不推荐这样做。

4. 属性的读写与删除规则

  • 读写未声明的属性:会直接报错,TS 不允许访问对象类型中不存在的属性。
  • 删除已声明的属性:会报错,TS 不允许删除类型声明中明确存在的属性。
  • 修改已声明属性的值:合法,只要值的类型与声明类型一致即可。
typescript 复制代码
const obj: {
  x: number;
  y: number;
} = { x: 1, y: 1 };

// 错误:读取不存在的属性 z(Property 'z' does not exist on type '{ x: number; y: number; }')
console.log(obj.z);

// 错误:写入不存在的属性 z(Property 'z' does not exist on type '{ x: number; y: number; }')
obj.z = 1;

const myUser: { name: string } = {
  name: "Sabrina",
};

// 错误:删除已声明的属性 name(The operand of a 'delete' operator must be optional)
delete myUser.name;

// 正确:修改属性值(类型匹配)
myUser.name = "Cynthia";

二、对象方法的类型声明

对象中的方法可以通过两种方式声明类型:函数签名语法箭头函数语法,两种写法效果一致,推荐使用函数签名语法(更贴近 JavaScript 方法的书写习惯)。

typescript 复制代码
const obj: {
  x: number;
  y: number;
  // 写法 1:函数签名语法(推荐)
  add(x: number, y: number): number;
  // 写法 2:箭头函数语法
  subtract: (x: number, y: number) => number;
} = {
  x: 1,
  y: 1,
  add(x, y) {
    return x + y;
  },
  subtract(x, y) {
    return x - y;
  }
};

上述示例中,方法 addsubtract 均声明了参数类型(两个 number 类型)和返回值类型(number 类型),实现时必须严格遵循该类型约束。

三、对象属性类型的读取

TypeScript 支持使用方括号 [] 读取对象类型中某个属性的具体类型,这在提取单个属性类型、实现类型复用场景中非常有用。

typescript 复制代码
// 定义完整对象类型
type User = {
  name: string;
  age: number;
  gender: boolean;
};

// 读取 name 属性的类型 → string
type UserName = User['name'];

// 读取 age 属性的类型 → number
type UserAge = User['age'];

// 实际使用:约束变量类型为 User 的 name 属性类型
const userName: UserName = "张三";
const userAge: UserAge = 22;

注意,方括号内的属性名必须用引号包裹(字符串字面量),且必须是对象类型中已声明的属性,否则会报错。

四、对象类型的两种别名方式:type vs interface

除了直接字面量声明,TypeScript 还提供了两种方式将对象类型提炼为可复用的别名:type 命令和 interface 命令,两者在对象类型声明场景中功能相似。

1. 两种方式的基本使用

typescript 复制代码
// 写法 1:type 命令(类型别名)
type MyObjType = {
  x: number;
  y: number;
};

const obj1: MyObjType = { x: 1, y: 1 };

// 写法 2:interface 命令(接口)
interface MyObjInterface {
  x: number;
  y: number;
}

const obj2: MyObjInterface = { x: 1, y: 1 };

2. 接口的特殊特性:继承属性的兼容

TypeScript 不区分对象「自身属性」和「继承属性」,一律视为对象的合法属性。因此,在接口中声明的继承属性(如 toString()),实现时无需手动提供(可从原型链继承)。

typescript 复制代码
interface MyInterface {
  // 继承属性:从 Object 原型链继承
  toString(): string;
  // 自身属性:必须手动提供
  prop: number;
}

// 正确:仅提供自身属性 prop,toString() 从原型链继承
const obj: MyInterface = {
  prop: 123,
};

上述示例中,obj 仅提供了 prop 属性,但仍符合 MyInterface 接口约束,因为 toString() 方法可从 Object 原型链中继承,无需手动实现。

五、可选属性

1. 可选属性的声明:?

如果某个属性不是必需的(可存在、可不存在),可以在属性名后添加问号 ? 标记为可选属性。

typescript 复制代码
// 声明 y 为可选属性
const obj: {
  x: number;
  y?: number;
} = { x: 1 }; // 正确:无需提供 y 属性

2. 可选属性与 undefined 的等价性

可选属性本质上等同于「允许赋值为 undefined 的属性」,下面两种写法完全等效:

typescript 复制代码
// 写法 1:简洁写法
type User1 = {
  firstName: string;
  lastName?: string;
};

// 写法 2:完整写法(明确允许 undefined)
type User2 = {
  firstName: string;
  lastName: string | undefined;
};

因此,可选属性可以显式赋值为 undefined,不会报错:

typescript 复制代码
const obj: {
  x: number;
  y?: number;
} = { x: 1, y: undefined }; // 正确

3. 可选属性的使用注意事项

读取未赋值的可选属性时,返回值为 undefined,直接调用该属性的方法会报错,因此使用前必须先进行 undefined 校验。

typescript 复制代码
type MyObj = {
  x: string;
  y?: string;
};

const obj: MyObj = { x: 'hello' };

// 错误:Cannot read properties of undefined (reading 'toLowerCase')
obj.y.toLowerCase();

常用的校验方式有两种:条件判断或 Null 合并运算符 ??(推荐)。

typescript 复制代码
const user: {
  firstName: string;
  lastName?: string;
} = { firstName: 'Foo' };

// 方式 1:条件判断
if (user.lastName !== undefined) {
  console.log(`hello ${user.firstName} ${user.lastName}`);
}

// 方式 2:Null 合并运算符(设置默认值,更简洁)
const firstName = user.firstName ?? '默认姓名';
const lastName = user.lastName ?? '默认姓氏';

console.log(`hello ${firstName} ${lastName}`);

4. 严格可选属性配置:ExactOptionalPropertyTypes

TypeScript 提供了编译配置 ExactOptionalPropertyTypes,当该配置与 strictNullChecks 同时开启时,可选属性将不允许显式赋值为 undefined,仅允许省略不写。

typescript 复制代码
// 开启 ExactOptionalPropertyTypes 和 strictNullChecks 后
const obj: {
  x: number;
  y?: number;
} = { x: 1, y: undefined }; // 错误:Type 'undefined' is not assignable to type 'number | undefined'

5. 可选属性 vs 允许 undefined 的必选属性

两者看似相似,但存在核心区别:可选属性可省略不写,而允许 undefined 的必选属性必须显式提供(即使值为 undefined)。

typescript 复制代码
type A = { x: number; y?: number }; // y 是可选属性
type B = { x: number; y: number | undefined }; // y 是必选属性(允许 undefined)

const objA: A = { x: 1 }; // 正确:可选属性可省略
const objB: B = { x: 1 }; // 错误:缺少必选属性 y(Property 'y' is missing in type '{ x: number; }')
const objB2: B = { x: 1, y: undefined }; // 正确:显式提供 y 并赋值为 undefined

六、只读属性

1. 只读属性的声明:readonly

在属性名前添加**readonly 关键字**,可将该属性标记为只读属性,初始化赋值后无法再修改其值。

typescript 复制代码
// 接口中声明只读属性
interface MyInterface {
  readonly prop: number;
}

// 直接字面量声明只读属性
const person: {
  readonly age: number;
} = { age: 20 };

// 错误:Cannot assign to 'age' because it is a read-only property
person.age = 21;

只读属性仅能在对象初始化期间赋值,此后任何修改操作都会被 TS 禁止。

typescript 复制代码
type Point = {
  readonly x: number;
  readonly y: number;
};

// 初始化赋值(合法)
const p: Point = { x: 0, y: 0 };

// 后续修改(非法)
p.x = 100; // 错误

2. 只读属性的深层注意事项

readonly 修饰符仅约束「属性本身的赋值」,如果属性值是一个对象(引用类型),readonly 不会禁止修改该对象的内部属性,仅禁止完全替换该对象。

typescript 复制代码
interface Home {
  readonly resident: {
    name: string;
    age: number;
  };
}

const h: Home = {
  resident: {
    name: 'Vicky',
    age: 42
  }
};

// 正确:修改对象内部属性(readonly 不约束深层属性)
h.resident.age = 32;

// 错误:完全替换 readonly 属性的值(Cannot assign to 'resident' because it is a read-only property)
h.resident = {
  name: 'Kate',
  age: 23
};

3. 只读引用的相互影响

如果一个对象有两个引用(一个可写、一个只读),修改可写引用的属性,会影响到只读引用(因为两者指向同一个对象)。

typescript 复制代码
interface Person {
  name: string;
  age: number;
}

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

// 可写引用
let writablePerson: Person = {
  name: 'Vicky',
  age: 42,
};

// 只读引用(指向同一个对象)
let readonlyPerson: ReadonlyPerson = writablePerson;

// 修改可写引用的属性
writablePerson.age += 1;

// 只读引用的属性值也会变化(输出 43)
console.log(readonlyPerson.age);

4. 只读断言:as const

除了声明时使用 readonly,还可以在对象赋值时使用**as const 只读断言**,将整个对象转为只读对象,所有属性均无法修改。

typescript 复制代码
const myUser = {
  name: "Sabrina",
} as const;

// 错误:Cannot assign to 'name' because it is a read-only property
myUser.name = "Cynthia";

注意:如果变量已经明确声明了类型,TS 会以「声明的类型」为准,as const 断言会被忽略。

typescript 复制代码
// 明确声明类型:name 为可写的 string 类型
const myUser: { name: string } = {
  name: "Sabrina",
} as const;

// 正确:以声明的类型为准,name 可修改
myUser.name = "Cynthia";

七、属性名的索引类型

当对象的属性名无法提前确定(如 API 返回的动态数据),或属性数量过多时,TypeScript 允许使用「属性名的索引类型」(又称索引签名)来描述对象类型,支持字符串、数字、Symbol 三种索引类型。

1. 字符串索引类型

最常用的索引类型,用于约束「属性名为字符串、属性值为统一类型」的对象。

typescript 复制代码
// 字符串索引类型:属性名是 string,属性值是 string
type StringMap = {
  [property: string]: string;
};

// 合法:所有属性均符合索引类型约束
const language: StringMap = {
  zh: "中文",
  en: "英文",
  ja: "日文",
};

其中,[property: string] 中的 property 是自定义的变量名,可任意修改(如 [key: string]),不影响类型校验。

2. 数字索引类型

用于约束「属性名为数字、属性值为统一类型」的对象,常用来描述类数组对象。

typescript 复制代码
// 数字索引类型:属性名是 number,属性值是 number
type NumberMap = {
  [index: number]: number;
};

// 合法:类数组对象
const arrLike: NumberMap = {
  0: 1,
  1: 2,
  2: 3,
};

// 也可用于简单数组(不推荐,数组有专门的类型声明方式)
const simpleArr: NumberMap = [1, 2, 3];

3. 索引类型的约束规则

  1. 数字索引服从字符串索引:JavaScript 内部会将数字属性名自动转为字符串,因此当对象同时存在数字索引和字符串索引时,数字索引的属性值类型必须兼容字符串索引的属性值类型,否则会报错。
typescript 复制代码
// 错误:数字索引类型(boolean)与字符串索引类型(string)不兼容
type MyType = {
  [x: number]: boolean;
  [x: string]: string;
};

// 正确:数字索引类型(string)兼容字符串索引类型(string)
type MyType2 = {
  [x: number]: string;
  [x: string]: string;
};
  1. 具体属性必须兼容索引类型:如果对象同时声明了具体属性和索引类型,具体属性的类型必须兼容索引类型的属性值类型,否则会报错。
typescript 复制代码
// 错误:具体属性 foo(boolean)与索引类型(string)不兼容
type MyType3 = {
  foo: boolean;
  [x: string]: string;
};

// 正确:具体属性 foo(string)兼容索引类型(string)
type MyType4 = {
  foo: string;
  [x: string]: string;
};

4. 索引类型的使用注意事项

索引类型的约束较为宽泛,容易忽略具体属性的细节校验,建议谨慎使用。另外,数字索引类型不宜用来声明数组,因为这种方式无法支持数组的 length 属性和各类数组方法(如 push()map())。

typescript 复制代码
type MyArr = {
  [n: number]: number;
};

const arr: MyArr = [1, 2, 3];

// 错误:Property 'length' does not exist on type 'MyArr'
console.log(arr.length);

// 错误:Property 'push' does not exist on type 'MyArr'
arr.push(4);

八、对象的解构赋值与类型声明

TypeScript 支持 JavaScript 的对象解构赋值,同时也需要为解构后的变量添加类型约束,其类型写法与对象类型声明一致。

1. 基本解构赋值的类型声明

typescript 复制代码
// 定义对象类型
type Product = {
  id: string;
  name: string;
  price: number;
};

// 定义原始对象
const product: Product = {
  id: "P001",
  name: "TypeScript 实战指南",
  price: 59.9,
};

// 解构赋值并添加类型声明
const { id, name, price }: {
  id: string;
  name: string;
  price: number;
} = product;

// 或直接使用已定义的类型别名(推荐,更简洁)
const { id: productId, name: productName }: Product = product;

2. 解构赋值中的冒号陷阱

需要特别注意:对象解构中的冒号 : 不是用来指定类型的,而是用来为属性重命名的。如果要为解构变量指定类型,必须在解构表达式外部整体添加类型声明。

typescript 复制代码
const obj = { x: "hello", y: 123 };

// 解构重命名:x → foo,y → bar(冒号不是类型声明)
let { x: foo, y: bar } = obj;

// 等同于
let foo = obj.x;
let bar = obj.y;

// 正确:外部添加整体类型声明
let { x: foo2, y: bar2 }: { x: string; y: number } = obj;

3. 函数参数解构的类型声明

在函数参数中使用对象解构时,同样需要注意冒号的用途,避免将重命名误认为类型声明。

typescript 复制代码
// 错误示例:将重命名误认为类型声明
function draw({
  shape: Shape, // 重命名:shape → Shape
  xPos: number = 100 // 错误:无法将类型 'number' 赋值给变量 xPos
}) {
  let myShape = shape; // 错误:找不到变量 shape
  let x = xPos; // 错误:找不到变量 xPos
}

// 正确示例:外部添加类型声明
function drawCorrect({
  shape,
  xPos = 100
}: {
  shape: string;
  xPos: number;
}) {
  let myShape = shape;
  let x = xPos;
  console.log(`绘制${myShape},x坐标:${x}`);
}

九、核心总结

  1. TypeScript 对象类型的基础声明使用 {},属性分隔符支持 ;,,赋值时需严格匹配「不多不少」的属性规则。
  2. 对象方法支持函数签名和箭头函数两种类型声明方式,属性类型可通过 [] 读取。
  3. typeinterface 均可定义对象类型别名,interface 支持继承属性的兼容。
  4. 可选属性用 ? 声明,本质等同于允许 undefined,使用前需做 undefined 校验;只读属性用 readonly 声明,仅约束属性本身的赋值。
  5. 索引类型用于处理动态属性名,支持字符串、数字、Symbol 三种类型,需遵循「数字索引服从字符串索引」的规则。
  6. 对象解构赋值的类型声明需在外部整体添加,解构内部的冒号用于重命名,而非类型指定。
相关推荐
jonjia30 分钟前
模块、脚本与声明文件
typescript
jonjia30 分钟前
配置 TypeScript
typescript
jonjia36 分钟前
TypeScript 工具函数开发
typescript
jonjia37 分钟前
注解与断言
typescript
jonjia38 分钟前
IDE 超能力
typescript
jonjia39 分钟前
对象类型
typescript
jonjia39 分钟前
快速搭建 TypeScript 开发环境
typescript
jonjia40 分钟前
TypeScript 的奇怪之处
typescript
jonjia43 分钟前
类型派生
typescript
jonjia44 分钟前
开发流程中的 TypeScript
typescript