Remix 在切换到 Vite 之后,热更新体验有了明显提升,但随之而来的是一些奇怪的类型问题:改一行样式,组件的 props 类型突然变成 any;保存文件之后,useLoaderData 的返回类型莫名丢失;甚至从某个工具模块导入的函数直接飘红,提示类型不存在。这类问题大多发生在 HLR/HMR 的模块边界处,本质上是 TypeScript 的类型信息与运行时模块图在热更新过程中不同步导致的。这篇文章就来拆解这个问题产生的原因,并给出几种经过验证的恢复方案。

为什么类型会在 HMR 边界丢失
要理解问题,首先要明白 HMR 的工作方式。Vite 的热更新只替换发生变化的模块,同时通过 import.meta.hot.accept 声明的边界模块会接收新版本模块的引用。Remix 的路由文件默认就是 HMR 边界,当你编辑一个路由组件时,Vite 只会重新执行这个路由模块,而不会重新执行它依赖的其他模块。
问题在于,TypeScript 的语言服务(tsserver)和 Vite 的模块运行时是两套独立的系统。tsserver 根据磁盘上的文件和项目的 tsconfig 来推断类型,理论上热更新不会影响它。但在实际开发中,几个因素会叠加导致类型异常:一是编辑器增量编译缓存陈旧,二是 Remix 的虚拟模块(如 virtual:react-router/server-build)在 tsserver 眼中没有对应声明,三是项目里存在多个互相冲突的类型声明文件。三者叠加,就会在 HMR 边界处表现为类型丢失或类型冲突。
一个典型的症状是:路由文件中从组件库导入的组件,在热更新后 typeof Component 变成了 any。此时关掉编辑器重新打开又恢复正常,这基本可以确认是 tsserver 的缓存问题,而不是代码本身的问题。区分症状来源是排查的第一步。
常见错误场景与排查方法
第一种场景是 useLoaderData 的类型丢失。Remix 的类型提示依赖路由文件中的 type loader 导出,如果这个导出是通过 satisfies 写的,且 tsconfig 中 target 或 module 配置过低,esbuild 在转换时会丢弃部分类型信息,导致编辑器推断失败。检查 tsconfig.json 中的配置:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"types": ["@remix-run/node", "vite/client"]
}
}这里最关键的是 moduleResolution 必须设置为 Bundler。如果还是 node 或 node16,Vite 项目的裸模块导入解析会和编辑器不一致,出现运行时正常但类型报错的分裂现象。
第二种场景是循环导入。HMR 边界处的模块如果和某个类型定义文件形成循环依赖,热更新后模块执行顺序会变化,类型可能在模块初始化时还未就绪。排查方法是用 madge 之类的工具扫描循环依赖,把纯类型导出拆到独立的 types.ts 文件中,并用 import type 显式声明,这样 esbuild 转换时会直接擦除这些导入,不参与运行时模块图。
// types.ts —— 纯类型文件,不产生运行时代码
export interface PostItem {
id: number;
title: string;
}
// routes/_index.tsx —— 使用 import type 隔离类型依赖
import type { PostItem } from "~/types";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader() {
return json({ posts: [] as PostItem[] });
}
export default function Index() {
const { posts } = useLoaderData<typeof loader>();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}第三种场景是重复的类型声明文件互相覆盖。有的项目在 src 下同时存在 remix.env.d.ts 和 env.d.ts,两个文件都做了 declare module,内容却不一致。tsserver 增量更新时可能加载到旧版本声明。建议只保留一个声明入口,并在其中统一声明虚拟模块:
// env.d.ts
/// <reference types="@remix-run/node" />
/// <reference types="vite/client" />
declare module "*.module.css" {
const classes: { readonly [key: string]: string };
export default classes;
}彻底恢复类型推断的实操方案
配置修正之后,还需要处理 tsserver 本身的缓存。VS Code 中可以按 Ctrl+Shift+P 打开命令面板,执行 TypeScript: Restart TS Server,这一步能解决大部分热更新后的假性类型错误。如果项目较大且问题反复出现,可以在 settings.json 中禁用 TypeScript 的自动语法检查,改用插件按需启动检查,减少增量编译的脏状态概率。
对于顽固的边界类型问题,可以在路由模块中显式标注类型,绕开推断链路。比如给组件的 props 手写 interface,给 loader 的返回值显式标注类型而不是依赖推断。这种做法虽然多写几行代码,但把类型的确定点从跨模块推断收回到本模块内,HMR 模块替换就不再影响类型可见性。
最后建议规范项目结构:类型统一放在 app/types 目录,路由文件只做 import type;每个模块的导出避免同时导出值和同名类型;升级 @remix-run 和 vite 插件到最新版本,早期版本的 remix vite 插件确实存在 HMR 后类型注入不完整的问题。按照以上步骤逐项排查,HMR 边界处的类型异常基本都能彻底消除,开发时的类型提示也会恢复到应有的可靠程度。
TypeScriptRemixHMR修改时间:2026-09-15 09:56:36