红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

TypeScript 学习笔记 4:对象类型

2026年2月24日 9点热度 0人点赞 0条评论

可以为对象类型的参数添加注解以规范对象定义:

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.

参考资料

  • TypeScript 官方手册

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: ts
最后更新:2026年2月24日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号