踩了个 TS 的坑之后,我终于把 type 和 interface 掰明白了

昨天写 UserCard 组件的时候,改个 props 类型改了半小时,控制台一直飘红,最后发现是把 interface 写成 type 还想重复声明搞合并,踩了个结结实实的坑。

事情发生在我这个 Vite + React + TS 的练手项目里,目录结构就是下面这样,当时正对着 components 下的 UserCard.tsx 磨类型定义。

最开始写组件的时候很顺,定义了一个 User 接口,再套一层 UserCardProps 当组件入参,渲染名字、年龄、邮箱,一切都安安稳稳的。后来想给用户信息加个头像字段,我犯了个懒,不想动原来的 interface 代码,随手在下面又写了个同名的 User 接口,把 avatar 加了进去。

按我当时的想法,重名了肯定要报错吧?结果保存完一看,TS 啥提示都没有,组件里直接读 user.avatar 居然能通过类型检查。我当时人都懵了,同样是定义对象,换个关键字待遇差这么多?

先说好:这俩大部分时候真的没区别

说实话,最开始学 TS 的前半个月,我真没把 type 和 interface 当两回事。反正都是用来给对象定结构的,写组件 props 用哪个都能跑,定义变量也都不报错,我甚至一度以为 interface 就是 type 的对象专属写法,换了个壳而已。

就像下面这段代码,不管用 interface 还是 type,定义出来的 u1 和 u2 用起来没有任何差别,传给组件也都能正常识别。

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

type UserType = {
    name: string;
    age: number;
    email: string;
}

const u1: User = {
    name: 'xjl',
    age: 18,
    email: 'xjl@example.com',
}

const u2: UserType = {
    name: 'xjl',
    age: 18,
    email: 'xjl@example.com',
}

写组件的时候也是,给 React.FC 传泛型,俩都能用。我最开始的 UserCard 组件就是用 interface 写的 props,跑起来一点问题没有。

tsx 复制代码
interface User {
    name: string;
    age: number;
    email: string;
}

interface UserCardProps {
    user: User;
    onEdit:(id: number) => void;
}

const UserCard:React.FC<UserCardProps> = ({user, onEdit}) => {
    return (
        <>
            <div>{user.name}</div>
            <div>{user.age}</div>
            <div>{user.useremail}</div>
        </>
    )
}

export default UserCard;

那时候我还觉得,既然都一样,那随便用就行了。直到踩了声明合并的坑,我才意识到事情没这么简单。

坑 1:同名会不会打架?声明合并是真的不一样

就是开头说的那件事,我想给 User 加字段,拆成了两个同名 interface,结果 TS 悄咪咪把它们合并了。我不死心,把 interface 换成 type 试了一下,当场就红了一大片,提示 "标识符重复"。

typescript 复制代码
interface Animal {
    name: string;
}

// 接口属性可以分多次约束,最后会自动合并
interface Animal {
    age: number;
}

const dog: Animal = {
    name: '旺财',
    age: 3,
}

上面这段是完全合法的,两个 Animal 接口会被合并成一个,同时拥有 name 和 age 属性。但如果换成 type,第二行直接就报错了。

typescript 复制代码
type AnimalType = { name: string }

// 报错:标识符"AnimalType"重复。
// type AnimalType = { age: number }

后来我去翻了文档才明白,interface 从设计上就是 "开放的",它的本职工作就是描述一个对象的结构,允许多次声明、逐步补充属性,这叫声明合并。而 type 本质是 "类型别名",相当于给一个类型起了个新名字,就跟 const 声明变量一样,同一个名字当然不能声明两次。

注意:只有 interface 支持声明合并,同名的 type 会直接报重复定义错误。如果你要扩展第三方库的类型、给全局对象加属性,用 interface 合并是常规操作;但自己写业务类型时,不建议把一个接口拆得到处都是,后期找属性能找疯。

坑 2:想扩展类型?写法完全不是一回事

既然说到给类型加字段,就绕不开 "继承 / 扩展" 这个话题。最开始我以为只有 interface 能继承,type 只能孤零零定义,后来才发现 type 也能做到类似的事,只是写法和思路完全不一样。

interface 用的是 extends 关键字,就是传统面向对象里 "继承" 的那套思路,子接口继承父接口的所有属性,再加上自己的。

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

interface Employee extends Person {
    age: number;
}

const e1: Employee = {
    name: 'xjl',
    age: 18,
}

而 type 没有 extends,它用的是交叉类型 &,意思是把两个类型的属性 "交叉" 到一起,取并集。

typescript 复制代码
type PersonType = { name: string }
type EmployeeType = PersonType & { age: number }

const e2: EmployeeType = {
    name: 'xjl',
    age: 18,
}

说实话,我一开始觉得这不就是写法不一样吗,效果都差不多。后来踩了个属性冲突的小坑才知道,俩玩意儿还真不是一回事。

比如两个类型里有同名但不同类型的属性,interface extends 的时候会直接报错,告诉你属性类型冲突;而 type 用交叉类型的话,会把两个类型合并成 never,不会立刻报错,直到你用的时候才发现不对劲。当然了,正常写业务代码很少会碰到这种情况,知道有这么个区别就行。

坑 3:不是所有类型,interface 都能写

如果说前面的区别还只是 "写法不同、效果相近",那这一点就是本质上的能力边界了 ------interface 只能描述对象结构,type 能描述所有类型

我刚学的时候干过一件傻事,想给 string | number 起个别名叫 ID,顺手写了个 interface ID = string | number,对着报错愣了五分钟,还以为自己语法写错了。后来才反应过来,interface 根本就干不了这个活。

typescript 复制代码
// 联合类型,type 轻松搞定
type ID = string | number;

// 元组类型,type 也能写
type Point = [number, number];

// interface 写不了联合类型,硬写只能得到一个空接口
// interface ID { }

像联合类型、元组、给 string/number 这种基础类型起别名,这些都只能用 type 来做。interface 的能力范围很单一,就是描述对象、类、函数的结构,超出对象范畴的事,它都管不了。

小结论:如果你的类型不是一个 "对象结构",比如是联合类型、元组、单个基础类型的别名,直接用 type 就对了,interface 处理不了这些场景。

坑 4:函数类型,俩都能写但手感不一样

函数类型也是平时写得很多的场景,interface 和 type 都能描述函数类型,但写出来的样子差很多。

interface 描述函数用的是调用签名的写法,长得像个只有参数和返回值的方法。

typescript 复制代码
interface AddFn {
    (a: number, b: number): number;
}

const add1: AddFn = (a, b) => a + b;

而 type 就直接得多,直接写一个箭头函数的类型结构。

typescript 复制代码
type AddType = (a: number, b: number) => number;

const add2: AddType = (a, b) => a + b;

平时写普通的函数类型,我更习惯用 type,短、直观,一眼就能看明白参数和返回值。interface 的写法一般只用在一种场景:这个函数本身还有额外的静态属性。比如一个请求函数,上面还挂着 cancel 方法,这种时候用 interface 的调用签名就更合适。

最后说句实在的:到底该用哪个

捋完这一圈,我再也不纠结 "谁更正宗" 这种问题了。TS 设计这两个东西本来就不是让你二选一的,它们各有各的适用场景,大部分时候重叠,少数时候互补。

说说我自己现在的用法,不一定标准,但写着顺手:

第一,写组件 Props、普通的对象结构,俩都行。我们团队习惯用 interface,那就跟着用 interface,也没什么毛病。如果是写工具类型、偏函数式的代码,我更倾向用 type。

第二,必须用 interface 的场景:需要声明合并的时候,比如扩展第三方库的类型、给全局 Window 加属性;还有配合 class 做面向对象编程、实现接口的时候,这是 interface 的本职工作。

第三,必须用 type 的场景:联合类型、元组、映射类型、条件类型这些高级类型玩法,还有给基础类型起别名。这些 interface 根本做不到,没得选。

说回最开始那个 UserCard 组件,最后我还是把属性合并回一个 interface 里了。拆成两个虽然能跑,但别人维护的时候找属性还要跳来跳去,纯属给自己和同事找麻烦。声明合并这个特性,留给扩展外部类型的时候用就好。

tsx 复制代码
// App.tsx 里调用的时候就是这样,类型稳稳的
function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <section id="center">
        <UserCard 
          user={{name: 'xjl', age: 18, email: 'xjl@example.com'}} 
          onEdit={() => {}}
        />
        {/* 其余代码省略 */}
      </section>
    </>
  )
}

其实学 TS 最忌讳的就是死抠概念,非要争出个高低。我最开始就是总想着 "哪个更规范",结果真写代码的时候踩了一堆坑。不如就对着报错一个个踩,踩完了自然就懂了。

如果你也有过类似的迷惑,或者有自己的选择习惯,评论区聊两句呗,我也想看看大家平时都是怎么用的。

相关推荐
GreenTea1 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
浮生望1 小时前
前端API工程化:用 Mock 数据与 Axios 配置实现独立于后端的并行开发
前端
万少2 小时前
给 DeepSeek Harness 装个"应用商店":一条命令,595 个插件随你逛
前端·javascript·后端
波波0072 小时前
C# 15 重磅新特性: 带标签 break 与 continue:重新定义嵌套循环控制流
服务器·前端·c#
Brown.alexis3 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝4 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps
mCell4 小时前
用 Cordis 从零构建一个 Mini DeepSeek Harness
typescript·agent·deepseek
计算机魔术师4 小时前
新兴多智能体系统的模式与问题
前端
用户059540174465 小时前
AI Agent 上下文污染踩坑实录:用 pytest + Redis 揪出 3 类记忆串号 bug,排查 6 小时
前端·css