3.10 TypeScript 与类型检查


文档摘要

3.10 TypeScript 与类型检查 本节摘要:类型检查分两条路线:TypeScript 在编译期拦错,PropTypes 在运行时提醒。本节从 TypeScript 的基础类型讲起,重点落在 React 组件上——props 接口、可选属性、React.FC、常用类型工具——再用 PropTypes 补上运行时检查的视角,最后给出「大型项目为什么选 TypeScript」的选型判断。 读前必看(上) 阅读完本节,你应当能够: 用接口定义组件 props 的类型 使用可选属性、联合类型、字面量类型等核心类型语法 用 React.

3.10 TypeScript 与类型检查

本节摘要:类型检查分两条路线:TypeScript 在编译期拦错,PropTypes 在运行时提醒。本节从 TypeScript 的基础类型讲起,重点落在 React 组件上——props 接口、可选属性、React.FC、常用类型工具——再用 PropTypes 补上运行时检查的视角,最后给出「大型项目为什么选 TypeScript」的选型判断。

读前必看(上)

阅读完本节,你应当能够:

  1. 用接口定义组件 props 的类型
  2. 使用可选属性、联合类型、字面量类型等核心类型语法
  3. 用 React.FC 与类型工具(Partial、Pick、Omit)描述组件
  4. 用 PropTypes 为组件 props 声明运行时检查
  5. 判断自己的项目该用 TypeScript 还是 PropTypes

一、问题与直觉:类型系统在帮谁

先看一个 JS 项目的日常:组件 A 传 props 给组件 B,B 假设 props.age 是数字。某天 A 忘了传 age,或者传了个字符串,B 渲染时炸了。JS 里这种错运行时才发现,而且往往在用户那边发现。

类型系统的核心价值是把「运行时错误」提前到「编写时发现」。TypeScript 在编译期检查类型,写错就报错,改完再跑——错误根本走不到用户面前。

SOURCE 原文把 TypeScript 定义为 JavaScript 的超集,添加静态类型定义。它不改变 JavaScript 的行为,只是让「类型约束」成为代码的一部分。

二、TypeScript 基础:给 JavaScript 加上类型

TypeScript 的编译流程:写代码 → tsc 检查 → 类型错误在编译期拦下 → 通过后编译成普通 JavaScript 再进浏览器。类型检查发生在编译阶段,所以它不改变运行行为,只是把「运行时才发现的错误」提前到「写代码时发现」。

基本类型与结构类型

let count: number = 5; let name: string = 'Alice'; let isDone: boolean = true; let ids: number[] = [1, 2, 3]; // 数组 let tuple: [string, number] = ['a', 1]; // 元组

TypeScript 的类型系统是「结构类型」——只要结构匹配,类型就匹配。这和后端语言的「名义类型」不同,写起来更灵活,也更符合 JS 的动态习惯。

对象类型与接口

描述对象用 interface:

interface Person { name: string; age: number; }

联合类型、字面量类型、类型别名

// 联合类型:值可以是字符串或数字 let id: string | number = 123; // 字面量类型:只能是指定的值 type Theme = 'light' | 'dark' | 'system'; let theme: Theme = 'light'; // 合法 theme = 'blue'; // 编译报错 // 类型别名:给复杂类型起名 type Status = 'pending' | 'done' | 'failed';

字面量类型和联合类型组合,是「枚举状态」的常用表达——比魔法字符串安全得多。

函数类型

type Add = (x: number, y: number) => number; const add: Add = (x, y) => x + y;

三、React 组件中的 TypeScript

用接口定义 props

这是 React + TS 的核心模式:

interface Props { name: string; age?: number; // 可选属性 } const MyComponent: React.FC<Props> = ({ name, age }) => { return ( <div> <h1>Hello, {name}!</h1> {age && <p>你 {age} 岁了。</p>} </div> ); };
  • interface Props 定义 props 的类型
  • name: string 必传字符串
  • age?: number 可选数字
  • React.FC<Props> 泛型声明这是接收 Props 的函数组件

使用组件时,TypeScript 检查传参:

<MyComponent name="Alice" age={30} /> // 正确 <MyComponent name={123} /> // 编译报错:name 必须是字符串

传错类型,编译期就拦住。这是 TS 在 React 里最大的价值——组件之间的契约在编译期检查

React.FC 与普通函数

React.FC 是老写法,现在官方建议函数组件直接用普通函数声明:

function MyComponent({ name, age }: Props) { return <div>...</div>; }

两种都能用,功能等价。React.FC 隐式支持 children,普通函数需要显式声明。团队统一一种即可,关键是 props 类型不能少。

更复杂的 props 类型

interface User { id: number; name: string; } interface TodoItemProps { todo: User; onToggle: (id: number) => void; // 函数类型 tags?: string[]; // 可选数组 }

函数作为 props 时,类型要写清楚参数和返回值。(id: number) => void 表示「接收数字、不返回任何值」——漏传参数或返回类型不符都会报错。

四、TypeScript 高级类型工具

TS 内置了一批操作类型的工具,React 里高频使用:

interface User { id: number; name: string; email: string; age: number; } type PartialUser = Partial<User>; // 所有属性可选 type ReadonlyUser = Readonly<User>; // 所有属性只读 type UserIDName = Pick<User, 'id' | 'name'>; // 只选 id 和 name type UserNoEmail = Omit<User, 'email'>; // 排除 email type StringMap = Record<string, number>; // 键字符串、值数字的对象
工具 作用 典型场景
Partial 所有属性变可选 表单编辑的草稿状态
Required 所有属性必选 补全可选字段
Pick<T, K> 挑出部分属性 列表项只需 id/name
Omit<T, K> 排除部分属性 创建对象排除 id
Record<K, T> 键值对对象 枚举映射

两条类型检查通道

两条类型检查通道

两张通道各管一段:TypeScript 在编译期全面拦截,PropTypes 在运行时针对 props 提醒。渐进迁移的项目可以短期双写——TS 内部约束、PropTypes 对外兜底。

泛型:类型的参数化

泛型让「类型」也能像参数一样被传入。React 里最熟悉的泛型是 useState——它的类型由初始值推断,也可以显式指定:

const [user, setUser] = useState<User | null>(null); // setUser 只能接收 User 或 null,传字符串会报错

自定义函数也可以用泛型,让工具函数适配多种类型:

function getFirst<T>(arr: T[]): T | undefined { return arr[0]; } const n = getFirst([1, 2, 3]); // n 的类型是 number const s = getFirst(['a', 'b']); // s 的类型是 string

理解泛型的关键是「类型参数」:它在调用时被具体类型替换,既保证类型安全又保持复用性。写通用工具、可复用组件时,泛型是必需品。

类型收窄:把宽类型变窄

联合类型(string | number)在使用前要先收窄——判断具体是哪个:

function format(value: string | number): string { if (typeof value === 'number') { return value.toFixed(2); // 收窄为 number } return value.toUpperCase(); // 收窄为 string }

typeof 判断后,TS 在分支内自动收窄类型。React 里处理「可能为 null 的数据」也是这个模式:if (user) { user.name },判空即收窄。类型收窄是 TS 日常里最常见的写法,掌握了它,null 检查、分支处理就不再报错了。

类型断言

TS 推断不出正确类型时,用 as 断言:

const element = document.getElementById('my-input') as HTMLInputElement; if (element) { element.value = 'Hello'; }

断言是「我比你懂」的声明,会绕过类型检查,谨慎使用。能用类型收窄(条件判断)就不用断言。

⚠️ 常见坑:滥用 as 把类型问题「藏起来」。as unknown as X 双断言更是双倍危险——它彻底关闭了类型保护。断言是逃生门,不是日常通道。

五、PropTypes:运行时检查的另一条路

PropTypes 是 React 内建的运行时检查机制,纯 JS 项目也能用。它不参与编译,而是在开发模式下运行时检查 props 类型,不匹配就打警告。

import PropTypes from 'prop-types'; const MyComponent = ({ name, age }) => { return ( <div> <h1>Hello, {name}!</h1> {age && <p>你 {age} 岁了。</p>} </div> ); }; MyComponent.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number, };

传错类型,控制台警告:

<MyComponent name={123} /> // 开发模式控制台警告:name 应为 string

PropTypes 的定位是「低成本入口」——不需要编译、不改变构建流程,纯 JS 项目贴上去就能用。它也适合给「从 JS 迁移到 TS 的路上」的项目临时兜底,边迁边用 PropTypes 提醒遗留问题。

TypeScript vs PropTypes

维度 TypeScript PropTypes
检查时机 编译期 运行时(开发模式)
是否需编译 需要 不需要
类型覆盖面 全面(变量、函数、结构) 仅 props
性能 无运行时开销 开发模式有少量开销
学习成本
适用 中大型项目 小型项目、渐进接入

六、选型判断

新项目、中大型项目、团队协作项目:直接用 TypeScript。 它带来的不只是类型检查——编辑器补全、重构安全、文档即代码,这些收益在项目变大后是刚需。

存量 JS 项目、小型项目、快速原型:TypeScript 可选,PropTypes 起步。 渐进迁移到 TS 的路径也成熟:先装 typescript 开启检查,再逐步给文件加类型。

一个诚实的提醒:TypeScript 有学习成本,短期内「写代码变慢了」。这个慢是「写的时候」慢,「改的时候」快——类型让重构成为受保护的行为。长期看,这笔投入在项目超过某个规模后回收得特别快。

常见疑问快答

「interface 和 type 到底该用哪个?」 这是 TypeScript 社区最常被问的问题。两者大部分场景可以互换,但有几处实质差异值得记住:interface 可以声明合并(同名 interface 会自动合并,方便扩展第三方库的类型)、可以继承(extends);type 支持联合类型、交叉类型、映射类型这些「类型运算」,interface 做不到。社区通行约定是「描述对象形状用 interface,需要类型运算用 type」——React 的 props 通常用 interface 就够了。关键是别纠结,两者混用也不会出问题,团队约定一种主用即可。

「为什么用了 TS 还是报『对象可能是 null』?」 因为 strict 模式下,TS 默认 null 和 undefined 不会被自动排除。document.getElementById 返回 HTMLElement | null,你不判空就访问 .value,TS 必须拦你——这是它「逼你处理真实边界」的设计。解法不是关 strict,而是学会判空、可选链、非空断言这组工具:if (el) 判空、el?.value 可选链、el!.value 非空断言(慎用)。习惯了之后你会发现,这些报错大多数时候「报得对」——真的有一类线上 bug 就是「元素还没挂载就访问了」。

「React.FC 到底建不建议用?」 现状是「历史推荐、官方不再推荐」。React.FC 的好处是隐式带 children 类型;但它的坏处也是隐式——你没法在类型里表达「这个组件不接受 children」,而且函数组件如今建议直接用普通函数声明(官方文档已改为这种示例)。结论:新代码用普通函数声明 + 显式 Props 接口,遇到旧代码里的 React.FC 能读懂即可,不必急着全量重写——它不影响运行,只是类型风格问题。

「PropTypes 和 TS 同时用会不会冲突?」 不会,但一般是重复劳动。TS 在编译期检查类型,PropTypes 在运行时检查,两者是「编译期与运行时」的两个保险。实际项目中「TS + PropTypes 双写」很罕见——TS 已经覆盖了 PropTypes 的大部分场景,PropTypes 的价值主要在「纯 JS 项目」或「第三方组件使用方没有 TS」时。渐进迁移场景里,双写可以短期共存:TS 在内部约束,PropTypes 对外部使用方兜底,迁移完成后撤掉 PropTypes。这个「过渡期双保险」的思路,比「一步到位全换」稳妥。

💡 关键直觉:类型系统是「契约的显式化」。props 的类型定义,等于告诉所有使用方「这个组件需要什么、返回什么」。TypeScript 让这个契约在编译期强制,PropTypes 让它运行时提醒。选哪个取决于你要「多早发现错误」。

七、工程实践:TypeScript 的落地要点

strict 模式必开。tsconfig 里 strict: true 开启最严格的检查,少踩「类型好像没检查到」的坑。开 strict 初期会有大量报错,但修完就一劳永逸。

any 是最后手段。any 关闭了类型检查,等于把 TS 降级回 JS。能用 unknown + 收窄,就别用 any。

类型定义单独组织。共享的接口、类型别名集中放,避免散落在组件文件里互相 import。

Hooks 也有类型。useState 的类型由初始值推断,useRef 要显式给类型(useRef(null))。不写对 ref 类型,访问 current 时会因为 null 报错。

常用 Hook 的类型写法

几个高频 Hook 的类型化写法,一次性给全:

import { useState, useRef, useEffect } from 'react'; // useState:初始值推断,或显式泛型 const [count, setCount] = useState(0); // number const [user, setUser] = useState<User | null>(null); // useRef:必须给类型,因为初始是 null const inputRef = useRef<HTMLInputElement>(null); inputRef.current?.focus(); // 可选链,处理 null // useEffect:依赖数组照常 useEffect(() => { // 副作用 }, [user]);

useRef 的 null 是 TS 里的经典难点:ref.current 在首次渲染时是 null,访问属性要可选链(?.)或判空。写成 useRef<HTMLInputElement>(null) 后,TS 会强制你在用之前处理 null——这正是类型系统的价值:它逼你处理「元素可能还没挂载」的真实情况。

TypeScript 常见的 React 报错

新手接 TS 最常碰到的几类报错,认识它们能省很多时间:

报错 原因 解法
name 类型不匹配 props 传错类型 检查 interface 定义
current 可能为 null ref 未判空 可选链 ?. 或判空
对象可能为 undefined 解构可选属性 给默认值或判空
函数返回类型不符 回调签名不对 按类型声明补全参数
implicit any 变量没类型 显式标注或让 TS 推断

绝大多数 TS 报错不是「类型写错」,而是「TS 在提醒你有真实的边界情况没处理」——对象可能是空、ref 可能是 null。把报错当成提示,而不是麻烦。

类型声明文件的组织

工程里 TS 的「类型管理」还有个组织问题:类型定义放哪。三种常见位置各有适用场景。第一,「内联定义」——类型只在单个组件内使用,直接写在组件文件里,就近、简单,适合私有类型。第二,「集中导出」——跨组件共享的类型(Props、数据模型、接口返回结构)集中放在类型模块里,统一导出,避免「每个组件各定义一份同名 Props」导致的漂移。第三,「声明文件」——给纯 JS 的第三方库补类型、或定义全局类型,用声明文件(.d.ts)。三个位置的判断标准是「复用范围」:只自己用的内联,多人用的集中,库级的进声明文件。类型组织得清楚,改数据模型时只需要动一处,其余靠编译检查定位引用——这是 TS 在「重构安全」上最直接的收益。

核心回顾

  • 类型价值:把运行时错误提前到编写时发现
  • 核心语法:interface、联合、字面量、函数类型
  • React props:interface 定义 Props,React.FC 或普通函数声明
  • 可选属性:age?: number,缺传或类型错都会报错
  • 类型工具:Partial、Pick、Omit、Record 等,表单与映射场景常用
  • 类型断言:as 谨慎用,能用收窄不用断言
  • PropTypes:运行时检查,纯 JS 项目起步方案
  • 选型:新项目大项目用 TS,小项目可先 PropTypes
  • 落地要点:strict 必开、any 少用、类型集中、Hooks 也要类型
  • 泛型:类型的参数化,useState/工具函数常用
  • 类型收窄:typeof/判空让联合类型变窄,null 检查标准姿势
  • 常见报错:把 TS 报错当「边界情况提示」,别当麻烦

下一章进入生态系统与工具——TypeScript 让代码可靠,工具链让项目高效。脚手架、状态库生态、UI 组件库、CSS 方案、常用工具,第 4 章把这些选择全部摊开,给你一套有依据的选型清单,第 3 章学到的所有选型逻辑都会在这里落地。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U