这节在地图里是"TS 落地到 UI"的第一站。React 和 Vue 都把类型用在组件边界上:props、事件、状态。契约写得好,改一个字段名时所有用错的地方立刻标红,这是 UI 层最大的类型收益。
import { FC } from "react"; interface ButtonProps { label: string; disabled?: boolean; onClick: (e: React.MouseEvent) => void; } const Button: FC<ButtonProps> = ({ label, disabled, onClick }) => ( <button disabled={disabled} onClick={onClick}>{label}</button> ); // 使用时:缺 onClick 或 label 类型错,编译期拦 <Button label="提交" onClick={() => {}} />; // OK // <Button label="提交" />; // 报错:缺必填 onClick
输入输出:正确传参编译通过;漏必填项立即标红。FC<ButtonProps> 把组件签名和 props 契约绑在一起。
useState/useRef 等自带泛型,传入初始值即可推断,或显式指定。
import { useState } from "react"; // 推断为 string const [name, setName] = useState(""); // 显式指定联合,初始可为 null const [user, setUser] = useState<{ id: number; name: string } | null>(null); // 自定义泛型 hook function useList<T>(initial: T[]): [T[], (v: T) => void] { const [list, setList] = useState(initial); const add = (v: T) => setList((p) => [...p, v]); return [list, add]; } const [nums, addNum] = useList<number>([1]); // nums: number[]
背景:想让一个列表 hook 服务多种数据类型且都保安全。操作:给 hook 加泛型 T。结果:用 number 时 addNum 只接受 number。解读:泛型把"复用"与"安全"同时给到 hooks。
function Input() { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { console.log(e.target.value); // 精确:e.target 是 input }; return <input onChange={onChange} />; }
React 提供全套事件类型(MouseEvent/ChangeEvent/FormEvent),精准描述 target 形状,比 any 强且不用手写。
这张图对比"无类型 props"与"类型化 props"的反馈差异:

import { ref, reactive, defineComponent } from "vue"; const count = ref(0); // 推断 Ref<number> const state = reactive({ name: "张三", age: 20 }); // 推断深层响应式 // defineComponent 让 props 获得类型 export default defineComponent({ props: { title: { type: String, required: true }, }, setup(props) { props.title; // 类型 string,必填 }, });
Vue 3 的组合式 API 对 TS 友好,ref/reactive 都能自动推断。组件用 defineComponent 包一层,模板里的 props 才有类型。
组件 props 通常用 interface(第三章),因为 props 常需要声明合并扩展(如高阶组件注入字段)。若项目统一用 type 也完全可行,关键是别混。
列表类组件常要服务多种数据,用泛型让"每项形状"贯穿到 render 回调。
interface ListProps<T> { items: T[]; keyOf: (item: T) => string; render: (item: T) => React.ReactNode; } function List<T>({ items, keyOf, render }: ListProps<T>) { return <>{items.map((it) => <div key={keyOf(it)}>{render(it)}</div>)}</>; } // 使用时 T 被推断,render 里 it 有完整类型 <List items={[{ id: 1, n: "a" }]} keyOf={(x) => String(x.id)} render={(x) => x.n} />;
输入输出:传入 {id,n}[] 后,render 的 x 自动是 {id:number; n:string},拼错 x.z 立即标红。泛型把"列表项契约"从组件签名一路带到渲染回调,是 React + TS 最高频的复用模式之一。
ref 在模板里自动解包,但在 TS 逻辑里要用 .value 访问,两者类型不同,易踩。
import { ref } from "vue"; const count = ref(0); count.value; // number,逻辑里必须 .value // 在模板 <div>{{ count }}</div> 中自动解包为 number // 若把 ref 直接当 number 用(count + 1)会类型错
背景:新手常把 ref(0) 当 number 直接用。操作:逻辑层始终 .value,模板层靠 Vue 自动解包。结果:类型对得上,运行也对得上。解读:ref 的"双层性"(包装对象 vs 解包值)是 Vue + TS 最该记牢的点,弄混会在编译期就报错,反而是好事。
跨组件共享数据时,createContext 配合泛型让"消费方拿到的 value"始终有类型。
import { createContext, useContext } from "react"; interface UserCtx { id: number; name: string; } const Ctx = createContext<UserCtx | null>(null); function useUser() { const u = useContext(Ctx); if (!u) throw new Error("需在 Provider 内使用"); return u; // 收窄后 u: UserCtx } // 消费处 u 有完整字段,拼错 u.age 立即标红
背景:全局用户、主题、配置常走 Context。操作:用泛型给 Context 定值类型,useContext 后收窄。结果:所有消费点共享同一契约。解读:Context 的"Provider/Consumer 解耦"容易让类型在消费端丢失,泛型把契约钉在创建处,下游一律受益。
Vue 3 的 emits 可配合类型描述"子组件向上抛的事件及其载荷",让父组件监听处获得提示与检查。
import { defineComponent } from "vue"; export default defineComponent({ emits: { // 声明事件并校验载荷 submit: (payload: { id: number }) => typeof payload.id === "number", }, setup(_, { emit }) { emit("submit", { id: 1 }); // OK // emit("submit", { id: "x" }); // 报错:载荷类型不符 }, });
背景:子父通信靠自定义事件,载荷常是对象。操作:在 emits 里用函数形式描述载荷类型。结果:父组件监听 submit 时拿到 { id: number }。解读:把"事件契约"写进组件定义,向上通信也有类型护栏,和 props 向下通信对称。
无论 React 还是 Vue,类型收益集中在"组件边界":props/state/emits/context。组件内部实现细节不必过度类型化,边界清楚即可。
类型化重点(投入产出比高): - 组件入参(props / emits 载荷) - 共享状态(Context / store 形状) - 列表项与回调(render / keyOf 泛型) 类型化次要(不必强求): - 纯内部局部变量(TS 推断已够) - 一次性计算的中间值
背景:有人想"全组件都写满类型",反而淹没重点。操作:把类型预算花在边界。结果:收益最大、维护最省。解读:框架集成的类型策略,本质是"在边界设卡"的又一次应用——和全书的契约主线一致。
FC<Props> 绑定组件与 props 契约defineComponent + ref/reactive 推断⚠️ 别在事件处理器里写 any 然后 e.target.value 随便取。React 事件类型能精确描述 target,放弃它等于主动丢掉 UI 层最便宜的一类保护。
💡 组件 props 优先 interface 并配 defineComponent/FC,让"改 props 名"这种重构能一次性红遍所有调用点,比靠测试覆盖更稳。