函数、对象与接口
如果说基础类型只是建立了“值有边界”这件事,那么函数和对象才是 TypeScript 真正开始发挥工程价值的地方。因为现实项目里的复杂度,大部分都不是来自一个孤立的string或number,而是来自“一个函数到底接收什么、返回什么”“一个对象到底有哪些字段、哪些字段可以没有、哪些字段绝对不能被改”。
换句话说,真正让 TypeScript 变得有用的,不是你会不会声明一个变量的类型,而是你能不能用类型把代码中的契约说清楚。
函数类型的核心,不是语法,而是契约
先看最基础的函数类型写法:
functionadd(a:number,b:number):number{returna+b;}这里看起来只有两个小细节:
- 参数类型:
a: number、b: number - 返回值类型:
: number
但从工程角度看,这已经是一份很明确的函数契约。它告诉调用方:
- 你必须传两个数字
- 我只会返回一个数字
- 如果你传字符串,或者期待它返回别的结构,那是错误用法
这正是 TypeScript 在函数层面的作用。它让函数不再只是“你去读代码才知道干什么”,而是签名本身就能传递大部分必要信息。
参数类型几乎总该明确写出来
局部变量可以多用推断,函数参数通常不建议省略。因为参数是边界,是外部进入当前逻辑的入口。边界模糊,后续类型链条就会一起变模糊。
functioncreateUser(name:string,age:number){return{id:Date.now(),name,age};}你也许会说,函数返回值这里没写类型。确实,返回值在很多简单场景里可以依赖推断。但参数最好尽量明确,因为它们直接定义了调用协议。
返回值类型什么时候该显式标注
返回值不是每次都必须手写,但在以下场景里,显式标注往往更值得:
- 核心业务函数
- 对外导出的公共函数
- 返回结构复杂的函数
- 不希望实现细节意外改变对外契约时
例如:
typeCreateUserResult={id:number;name:string;age:number;};functioncreateUser(name:string,age:number):CreateUserResult{return{id:Date.now(),name,age};}这样做的好处是,如果某天有人把id改成了字符串,或者返回结构被悄悄修改,TypeScript 会立即提醒你。
可选参数、默认参数和剩余参数
可选参数
functiongreet(name:string,title?:string){returntitle?`${title}${name}`:name;}title?: string表示这个参数可以不传。注意,“可选”不是“随便乱传”,它的含义是:这个位置要么不存在,要么是string。
默认参数
functioncreatePage(pageSize:number=10){return{pageSize};}默认参数让函数更好用,但它本质上仍然是一个明确的输入边界,而不是放弃约束。
剩余参数
functionsum(...nums:number[]):number{returnnums.reduce((total,current)=>total+current,0);}剩余参数在工具函数、事件处理、转发函数里很常见。关键点是:即便参数个数可变,元素类型依然应该清楚。
对象类型是业务建模的真正起点
大部分业务代码,最终都绕不开对象。用户、订单、配置、组件 props、接口响应,本质上几乎都是对象结构。
constuser:{id:number;name:string;active:boolean;}={id:1,name:"Alice",active:true};这种内联写法在小范围里没问题,但一旦结构重复出现,就应该提取出来。原因很简单:重复结构意味着重复维护,而重复维护迟早会失控。
type和interface都能描述对象,但思维略有不同
你可以这样写:
typeUser={id:number;name:string;active:boolean;};也可以这样写:
interfaceUser{id:number;name:string;active:boolean;}初学阶段,两者最重要的区别不是语法能力,而是使用语义:
interface更像“对象契约”type更像“类型表达式的别名”
如果你描述的是一个清晰的对象结构,interface很自然;如果你要组合联合类型、函数类型、元组、映射类型,type往往更灵活。
在没有团队规范时,我个人更建议:
- 对象边界优先用
interface - 组合类型、工具型类型优先用
type
这不是绝对规则,但这种分工通常更利于阅读。
函数类型也应该被当成一等公民
很多项目里,函数不只是“实现逻辑”,它本身也经常作为参数、配置项或策略注入出现。这时,函数类型就很值得抽离:
typeFormatter=(value:string)=>string;constupperCase:Formatter=(value)=>value.toUpperCase();consttrimText:Formatter=(value)=>value.trim();你会在这些场景里频繁见到这种写法:
- 数组方法回调
- 表单校验器
- 组件事件回调
- 中间件
- 策略模式
当一个函数类型会被复用,单独给它命名,比反复写长签名清晰得多。
可选属性和只读属性,是表达业务语义的重要方式
interfaceProduct{readonlyid:number;name:string;description?:string;}这里的两个小语法,实际项目里非常有价值。
readonly
readonly不是为了防止程序员手滑,它表达的是业务语义:这个值一旦生成,就不应该被重新赋值。比如数据库主键、创建时间、订单编号,这些字段很适合只读。
?
可选属性不是“这个字段可以乱来”,而是“这个字段在合法数据里允许不存在”。这对于头像、备注、简介、扩展信息等很常见。
一个类型写得好不好,很大程度上就看这些边界有没有说清楚。
一个更接近真实项目的例子
interfaceUserProfile{readonlyid:number;name:string;email:string;avatar?:string;bio?:string;}functionupdateUserName(user:UserProfile,nextName:string):UserProfile{return{...user,name:nextName};}从这个例子里你应该能感受到,类型系统的意义不是限制你,而是帮你把对象的合法形态表达得更稳定:
id不能乱改name和email是核心字段avatar和bio可以没有
这类结构一旦清楚,后面的函数、接口、组件都更容易围绕它工作。
一个工程判断标准:类型有没有把业务意图说出来
写 TypeScript 时,别只问“这样能不能通过编译”,还要问:
- 这个字段是否应该只读
- 这个属性是否真的可选
- 这个函数的参数是不是表达了真实前提
- 这个返回值是不是把外部契约说清楚了
如果你的类型只是“勉强让编辑器不报错”,那它的价值会很有限。真正好的类型,是读者不看实现细节,也能大概理解这段代码能做什么、不能做什么。
常见误区
误区一:对象一复杂就直接any
这会让最该被建模的部分恰好失去保护。复杂对象更应该拆清楚,不该直接逃避。
误区二:所有返回值都不写类型
简单局部函数可以省,但公共函数和核心逻辑如果完全依赖推断,后续实现变化时更容易无意中破坏契约。
误区三:把type和interface之争当成重点
真正重要的不是你站哪一派,而是你能不能稳定地写出清晰的结构定义。语义一致、团队一致,通常比“理论上谁更优雅”更重要。
本文小结
函数、对象与接口,是 TypeScript 建模的主战场。基础类型解决的是“一个值是什么”,函数和对象解决的是“系统如何协作、边界如何表达”。你如果能把函数契约写清楚、把对象结构建模准确,就已经掌握了 TypeScript 最有实际价值的一部分。
练习
- 给一个“创建用户”的函数补上参数类型和返回值类型,并思考哪些字段应该由调用方传入,哪些字段应该由系统生成。
- 用
interface定义一个Book,包含只读id、必填title、可选author、可选description。 - 写一个函数类型
Validator,表示接收字符串并返回布尔值,然后实现两个不同的校验函数。
后记
2026年5月21日于上海。