最近在React Native社区里经常看到有人把js、jsx、ts、tsx混着用也看到不少刚入门的朋友问“这四个到底有啥区别”。我最早接触RN的时候也走过弯路项目里既有.js又有.jsx后来升级TypeScript又冒出来.ts和.tsx当时被这些文件后缀整得有点晕。这四种扩展名看着相似实际背后是两条不同的轴线在起作用一条是“有没有JSX语法”一条是“有没有TypeScript类型系统”。搞懂这两条轴线这四个后缀的关系就非常清晰了。今天这篇就从这两条轴线下手结合React Native的实际开发场景把每种文件的定位、用法、选型讲透顺便分享一些我在真实项目里踩过并沉淀下来的坑。1. 拆开看JSX和TypeScript是两个维度的东西先说结论js、jsx、ts、tsx不是四个并列的概念而是由两个独立维度组合出来的四种结果。第一个维度是“是否使用JSX语法”第二个维度是“是否使用TypeScript类型系统”。把这个两个维度正交组合就得到了我们看到的四种文件后缀不用JSX、不用TS就是原始的JavaScript后缀.js用JSX、不用TS带JSX语法的JavaScript后缀.jsx不用JSX、用TS带类型系统的JavaScript超集后缀.ts用JSX、用TS两者都用后缀.tsx这个四象限关系可以用一张表格直接看明白是否使用JSX是否使用TypeScript文件后缀RN项目里的位置否否.js极少数纯逻辑工具文件是否.jsx早期RN组件文件否是.ts类型定义、纯工具函数、接口定义是是.tsx新项目里的主力组件文件好接下来挨个解释清楚每个维度到底是什么。1.1 JSX是什么为什么React和RN都在用JSX是一种JavaScript的语法扩展它允许你在JavaScript代码里直接写类似HTML的标签结构。比如const element Text style{styles.title}Hello World/Text;这段代码里出现了Text这样的标签这在纯JavaScript语法里是不合法的。但JSX把这段“像HTML又不是HTML”的结构编译成React.createElement调用最终生成React元素。用生活类比JSX像是一种“模板化的JavaScript”。它让你在写逻辑的同时能“手写界面结构”比纯JavaScript拼接字符串、堆对象直观得多。React Native里每个界面都是由View、Text、Image、ScrollView这些核心组件嵌套出来的用JSX描述这些嵌套结构代码读起来几乎和看HTML一样清晰。关键点是JSX不是一个独立的新语言它只是JavaScript的一个超集编译阶段。Babel这类编译器会把JSX转换成标准JavaScript这一步在React Native的打包链路里是自动完成的。1.2 TypeScript是什么它解决了什么问题TypeScript简称TS是微软推出的JavaScript超集核心贡献是给JavaScript加入了静态类型系统。所谓“静态类型”简单理解就是变量和函数的输入输出参数在代码写下来的时候是确定可见的而不是等到运行期才暴露问题。比如下面这段纯JavaScript代码function formatPrice(price) { return ¥${price.toFixed(2)}; }如果你不小心传入了一个字符串的99.toFixed方法就会在运行期直接抛错。而TypeScript版本function formatPrice(price: number): string { return ¥${price.toFixed(2)}; }编辑器会在你写代码的时候直接提示“你传错了类型”这就把一大批运行期错误提前到了编辑期。用生活类比JavaScript就像没有菜单的餐厅你只有等菜端上桌才知道点错了TypeScript则是带图片、带配料表、带热量标注的菜单上来前就能知道这道菜长什么样、含什么成分。对于React Native这种跑在移动端的场景TypeScript的价值更大——因为手机上没法像浏览器那样随时打开DevTools调试编译期拦截错误能省掉大量真机排查时间。1.3 为什么RN项目选型要看两条轴线理解了JSX和TS之后再看那四个后缀就非常清楚了。两条轴线分别解决两个完全不同的需求JSX解决的是“界面怎么描述”的问题TypeScript解决的是“类型怎么约束”的问题一个RN项目里这两种需求往往是同时存在的既要用JSX写界面又想用TypeScript约束类型。于是就有了.tsx。也有人问既然.js也能写JSX很多老项目就这么干的为什么还要分.jsx这在工具链层面有实际意义.jsx扩展名能明确告诉Babel和编辑器“这个文件里可能有JSX语法”从而启用对应的语法解析插件而.tsx则同时告诉TypeScript编译器“这个文件里既可能有JSX又可能有TypeScript语法需要组合解析”。扩展名不是摆设它是整个工具链识别文件内容的“身份证”。2. 四种文件各管什么React Native里的真实定位四种扩展名对应四种不同的“角色分工”。理解了每个文件的典型使用场景你写代码的时候自然会形成一种“该用谁就用谁”的条件反射。2.1 纯js文件React Native里的“遗留角色”在当前的RN开发体系里.js文件其实是比较尴尬的。新的RN官方模板已经从纯JavaScript切换到了TypeScript微软和Meta官方都在推动TS成为默认选择。那么.js文件还有存在感吗有但基本都是下面这几种情况老项目从早期RN版本带过来的存量代码一些第三方npm包本身用纯JS编写原生模块桥接层里偶尔出现的纯逻辑脚本配置文件比如metro.config.js、babel.config.js这些从迁移的角度看纯.js文件的维护成本会越来越高。因为一旦你引入了TypeScript.js文件就处于“类型游离状态”——它既不受类型检查约束又无法为其他.ts/.tsx文件提供类型推导依据。个人实测的经验是老项目里的.js组件文件在迁移过程中是最容易出问题的。它们里头的props往往没有类型定义一旦周围的.ts文件引用它们TypeScript编译器会推断出一个宽泛的any类型久而久之类型检查形同虚设。所以我对RN新老项目的一个通用建议是新写代码一律不要用.js除非你是写metro.config.js这类不带业务逻辑的配置文件。2.2 纯ts文件RN项目的“逻辑层和类型层”.ts文件在一流RN架构里承担的是“不涉及界面描述”的职责。它主要出现在这些地方类型定义types.ts网络请求封装api.ts工具函数库utils.ts状态管理store比如zustand或redux的reducer、selector自定义Hook逻辑如果这个Hook只处理数据、不返回JSX.ts是纯TypeScript没有任何JSX标签。它的优势是类型系统完整、意图明确、可复用性强。比如你的项目里有一个UserInfo接口定义在types.ts里那么这个接口可以被很多.tsx组件引用公共类型有了唯一出口这就避免了每个组件各自写一遍松散的类型声明。在RN项目里我基本遵循一个原则不需要写界面结构的文件就用.ts需要写界面结构的文件就用.tsx。这个原则不需要死记写代码时本能反应就对了。因为一旦你在.ts文件里写了一个View /标签编辑器会直接报语法错误反过来也一样——.tsx文件里如果没有任何JSX标签TypeScript编译器也能正常解析只是没什么必要。2.3 纯jsx文件RN项目里的“过渡产物”.jsx文件是标准的“带JSX语法的JavaScript文件”它在React Native早期2015–2018年左右是组件文件的主力。那时候的RN模板就是.js和.jsx混着用没有TypeScript做类型检查。放在今天的RN项目里.jsx的处境有点像“夹生饭”你说它能用吧它确实能用你说它现代吧它又少了类型系统这层保护。所以我的判断是——新项目不要再新建.jsx文件老项目里的.jsx文件在迁移时应该顺手转成.tsx。可能有人会问“我项目里已经有一堆.jsx了能先放着不管吗”短期看可以长期看是负担。因为你在新代码里是.tsx老代码里是.jsx整个项目就存在两套智商体系。编辑器里老文件的代码提示永远是残缺的因为你没写类型它也没法自动推导出比any更具体的东西。如果你是React Native初学者建议直接绕开.jsx这个阶段从.tsx开始写。JSX语法本身你需要学TypeScript类型注释你也需要学但没必要非要在.jsx这个带JSX但不带TS的“过渡形态”里花费时间。2.4 tsx文件React Native开发的当前主力.tsx文件是目前RN开发里最主流、最推荐的文件格式。它同时具备JSX的界面描述能力和TypeScript的类型约束能力一个文件里既能写组件标签又能定义props类型、状态类型、返回类型。举个实际例子一个用.tsx写的RN按钮组件import React from react; import { Pressable, Text, StyleSheet } from react-native; type CustomButtonProps { label: string; onPress: () void; variant?: primary | secondary; disabled?: boolean; }; export default function CustomButton({ label, onPress, variant primary, disabled false }: CustomButtonProps) { return ( Pressable style{[ styles.base, variant primary ? styles.primary : styles.secondary, disabled styles.disabled ]} onPress{onPress} disabled{disabled} Text style{styles.label}{label}/Text /Pressable ); } const styles StyleSheet.create({ base: { paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, }, primary: { backgroundColor: #2563EB, }, secondary: { backgroundColor: #E5E7EB, }, disabled: { opacity: 0.5, }, label: { color: #FFFFFF, fontSize: 16, fontWeight: 600, textAlign: center, }, });这里CustomButtonProps定义了组件的对外接口label必须是字符串onPress必须是函数variant只能是两个枚举值之一。任何用这个组件的人把onPress传错成字符串、把variant写成枚举外的值编辑器直接报红。这就是.tsx给React Native开发带来最大的直观价值你写的组件变成一个“带说明书的API”而不是一个对方用错了才炸的封装。React Native官方模板目前也是基于TypeScript搭建的如果你用react-native init创建新项目并选择TypeScript模板生成的文件里组件部分基本全是.tsx。3. 选型与实操React Native项目要怎么做才是最优配置说完四者的定位落到实际项目里桩桩件件都是配置细节。这一节我用“从一个空目录到可运行的RN项目”的视角梳理一份最推荐的文件组织方案。3.1 新项目从项目模板开始就锁定三种扩展名如果你用现代方式创建RN项目npx react-native-community/cli init MyApp --template react-native-template-typescript生成的项目里几乎看不到.jsx文件组件的默认扩展名是.tsx逻辑文件是.ts配置类文件是.js。个人实测下来这个模板组合是当前RN项目最舒服的配置原因是组件统一.tsx类型统一由.ts导出全项目只有两套扩展名认知负担最低工具链Babel、Metro、TypeScript编译器全部默认支持这种组合不需要额外折腾任何解析器配置官方模板维护团队会持续跟进RN底层升级你不用操心依赖版本匹配问题在一个体积适中的RN项目里规范文件职责后目录结构通常长这样src/ ├── components/ │ ├── Button/index.tsx # 组件本体 │ ├── Button/types.ts # 组件对外接口类型 │ └── ProductCard/index.tsx ├── screens/ │ ├── HomeScreen/index.tsx │ └── ProfileScreen/index.tsx ├── services/ │ ├── api.ts # 网络请求封装 │ └── storage.ts ├── hooks/ │ └── useUserData.ts # 无JSX纯Hook逻辑 ├── types/ │ └── models.ts # 业务模型类型定义 └── utils/ └── format.ts你注意这个结构里只有“带界面的东西”才会出现.tsx。而api.ts、storage.ts、format.ts、types.ts这些文件里没有任何一个View所以它们用.ts就够了。3.2 老项目迁移从.jsx到.tsx的稳妥路线事如果你手上是一个从早期RN版本留下来的老项目里面全是.js和.jsx文件直接一次性换成.tsx风险较高特别是大项目。我建议分三步走第一步先把jsconfig.json升级成tsconfig.json让TypeScript编译器介入但不强制报错strict: false保证存量代码能编译。这一步相当于给老项目装了一个“类型检查的观察镜”先看不改。第二步把纯逻辑文件工具函数、API封装、常量定义从.js改成.ts因为它们不涉及JSX迁移最安全。改完跑一遍全量编译确认没有类型错误后再继续。第三步从叶子组件往上迁组件文件。先把不依赖其他组件的纯展示组件从.jsx改成.tsx给props补上类型再处理容器组件。最后再处理页面级组件。每一次迁移范围控制在最小都跑一遍测试和编译。老项目迁移过程中最常遇到的一个坑是一个.jsx组件被很多地方引用但作者没有写过任何props类型。你把它改成.tsx后每个引用点都会爆出一堆类型错误。这里我的经验是不要硬着头皮全改先给这个组件写一个外部的类型定义比如ComponentName.types.ts然后逐个修正引用点。如果涉及外部依赖的props实在没有类型定义可以先给一个合理的联合类型或Partial类型兜底等后续慢慢收紧。3.3 tsconfig配置要点别让四类文件产生编译误差保证.js、.jsx、.ts、.tsx四类文件能和谐共处核心在tsconfig.json里几项配置allowJs、checkJs、jsx、strict。一个RN项目常用的tsconfig.json参考配置如下{ compilerOptions: { allowJs: true, allowSyntheticDefaultImports: true, esModuleInterop: true, isolatedModules: true, jsx: react-jsx, lib: [esnext], moduleResolution: node, noEmit: true, strict: true, target: esnext }, extends: react-native/typescript-config/tsconfig.json }这里重点解释几个关键项allowJs: true允许TypeScript编译器处理.js文件这样你老项目里的存量.js文件不会被编译器忽略可以参与模块解析jsx: react-jsx是React Native新版本的推荐配置它让编译器自动转换JSXnoEmit: true只做类型检查不输出编译产物因为RN项目真正的打包走的是Metro/BabelTS编译仅用于类型校验需要留意的是checkJs这个配置项。如果你设了checkJs: trueTypeScript会对.js文件也做类型检查那老项目里的几百个.js文件全部会被类型错误淹没别开。3.4 为什么不是“无脑全上.tsx”说完推荐配置也说说反直觉的部分。很多人会觉得“那我是不是干脆所有文件都叫.tsx算了”真不推荐原因有二第一.tsx文件里如果误写了一个带有泛型语法的表达式会和JSX冲突。比如说你有个工具函数// 这个写法在.ts文件里合法 function identityT(value: T): T { return value; }这个泛型箭头函数写法如果放在.tsx文件里TypeScript编译器会把T理解为JSX的开始标签直接报语法错误。对应的修复办法是给泛型参数加逗号尾缀或者改用函数表达式但这些别扭写法的出现就是在提醒你纯逻辑根本不该放.tsx里。第二.tsx文件越多编译器每次类型检查时需要解析的JSX标签越多编译速度会更慢。虽然现在机器性能很可观但大项目里这一点点差异还是会积少成多的。需求很纯粹界面归.tsx逻辑归.ts配置归.js。4. 常见报错与排查这几种坑我从实际项目里都踩过类型系统和JSX语法一起用能拦截很多问题但配置不当也会制造不少幺蛾子。下面几个是RN项目里最常见的基本属于必踩坑系列。4.1JSX element implicitly has type any报错怎么办这个报错出现频率极高尤其是在老项目里刚把文件改成.tsx的时候。原因是一个组件从另一个.js文件导入而这个导入源没有类型定义TypeScript推导不出它的类型最终整个组件实例被推断为any。典型场景// ProductCard.tsx import ProductBadge from ./ProductBadge; // 这个是.jsx文件没有类型解决办法分为三种把被导入的.jsx文件顺带改成.tsx并补上类型定义治本暂时在目标文件顶部写一行// ts-ignore跳过这条检查后续再修在src目录下放一个declarations.d.ts文件给无类型的模块声明兜底// declarations.d.ts declare module *.jsx { const component: React.ComponentTypeany; export default component; }我自己遇到这种情况时优先执行方案1如果因为工作量排期问题没法立刻改完就用方案3做个全局兜底。用ts-ignore是下策因为它会掩盖同一文件里其他真实的类型问题。4.2Cannot find module ./xxx or its corresponding type declarations报错这种报错常见于从.js文件导入图片、音频等资源的时候。在纯JavaScript场景下没有类型检查概念但.tsx里导入一张图片import bannerImage from ./assets/banner.png;如果没有declarations.d.ts里的资源类型声明TypeScript会直接报“找不到模块”但实际上编译是能通过的。解决方案是补一个全局资源声明declare module *.png { const value: number; // React Native里图片资源pad.png导入后是一个数字ID export default value; }顺带提一个RN特性React Native中import img from ./a.png得到的不是一个URL字符串而是一个打包后的资源模块引用通常是数字或对象所以类型声明时不要照搬web项目的string写法。4.3 运行正常但Editor里到处是红波浪线的“假阳性错误”有一种情况非常让人迷惑项目能正常编译、真机运行完全没问题但编辑器里一堆红波浪线。大概率是tsconfig.json配置与Metro/Babel的解析规则不一致导致的。最常见的是路径别名配置不一致。比如你项目用Babel配置了/作为src目录的别名但tsconfig.json里没同步配paths// Babel能解析 import { api } from /services/api; // 但TypeScript不知道是什么于是报错解决办法是让两边配置对齐{ paths: { /*: [src/*] } }这个问题的根源在于RN项目存在两套解析体系打包走的是Metro和Babel类型检查走的是tsc。两边配置不一致就会出现“编译能过但编辑器报红”这种割裂状态。我在实际操作中的习惯是每改一次babel.config.js或新增一个别名都会同步检查tsconfig.json。4.4 组件能运行但props完全没有类型提示有一种更隐蔽的情况你确认自己在写.tsx也写了props类型但编辑器在另一个文件引用该组件时依然完全不提示props属性。这通常不是类型定义的问题而是组件导出方式的问题。常见写法// 反例默认导出一个匿名组件类型推导效果差 export default function ProductCard(props) { // props没有标注类型编辑器无法推导 }不规范的props会导致编辑器在你调用这个组件的时候只能提示“这个东西接受一个props”但具体props里有什么字段一概推不出来。正确的写法是定义接口type ProductCardProps { title: string; price: number; onPress: () void; }; export default function ProductCard({ title, price, onPress }: ProductCardProps) { // ... }约定优先于配置。只要项目里三分之二的组件都遵循“props先定义type再拆解”的写法整体开发体验就能上去一个层级。4.5 RN新架构模式下TypeScript的额外价值最后补充一个贴近当前工程化的视角——React Native从新架构New Architecture逐步铺开之后TypeScript的作用进一步变大了。新架构中组件和原生模块之间的接口定义涉及跨语言类型生成即所谓的Codegen而Codegen直接依托TypeScript类型定义来生成原生语言代码。这意味着.ts和.tsx里写下的类型不再只是“开发期约束”它们还承担着自动生成桥接代码的职责。这带来的直接后果是如果你的业务接口类型定义写得乱新架构的Codegen阶段就报错越早用TS、类型定义越清晰越早摆脱老式桥接手动维护的繁琐。从长期看在RN生态里深耕的开发者很难绕过这个方向。5. 一点个人经验在RN项目里文件后缀看起来是一件很小的“命名问题”但它在项目进入维护期后直接影响可读性、可维护性和团队协作效率。我自己写过纯js裸奔的老项目也写过全量tsx的从零启动项目对比下来最直观的感受是类型系统在刚开始写的时候确实多花了一点时间但到了开发中后期几乎每天都能感受到它省下的时间和减少的崩溃排查。给刚起步的RN开发者一个最实用的建议新项目直接从.tsx开始纯逻辑放.ts配置项用.js。别回头去看.jsx时代的旧写法也别迷信四种后缀有什么高深莫测的门道。这两条轴线想明白后缀的选用完全是顺手的事。再顺手分享一个我在团队里的约定任何新建的组件文件如果没有特殊原因统一用.tsx任何新建的纯函数文件统一用.ts任何提测Merge Request里如果出现.jsx文件review时必须追问一句“为什么这里不迁成.tsx”。执行这一条约定几个月后团队的项目里基本就看不到.jsx了大家写代码时的类型提示也稳定在一个比较舒服的水平上。