本文是《TypeScript 从入门到实战》系列的上篇,聚焦类型系统的核心概念,并结合 React 开发场景循序渐进地讲解。如果你还在用 JavaScript 写 React,看完这篇你会知道为什么值得切换到 TypeScript。
一、为什么要用 TypeScript?
先来聊聊一个真实场景:
// JavaScript 代码,某天生产环境报错了
function getUserAge(user) {
return user.profile.age; // TypeError: Cannot read properties of undefined
}
getUserAge({ name: 'leon' }); // profile 是 undefined!
这种错误在 JavaScript 中极其常见,只有运行时才会暴露。而 TypeScript 在编写代码时就能捕获:
interface User {
name: string;
profile?: {
age: number;
};
}
function getUserAge(user: User): number | undefined {
return user.profile?.age; // 编译器提醒你 profile 可能不存在
}
TypeScript 的核心价值在于:
编译时类型检查:错误在写代码时暴露,而非运行时 智能代码补全:IDE 能精准提示属性和方法 自文档化代码:类型定义本身就是最好的文档 安全重构:改了一个地方,所有受影响的位置立即飘红
二、基础类型
2.1 原始类型
// 基本类型注解
const name: string = 'leon';
const age: number = 28;
const isActive: boolean = true;
const nothing: null = null;
const notDefined: undefined = undefined;
// 大整数
const bigNum: bigint = 9007199254740991n;
// Symbol(唯一值)
const sym: symbol = Symbol('id');
实际开发中,TypeScript 有类型推断能力,很多时候不需要显式写类型:
// 自动推断为 string,不需要手动标注
const name = 'leon';
// 自动推断为 number[]
const scores = [90, 85, 78];
2.2 数组与元组
// 数组类型的两种写法
const nums: number[] = [1, 2, 3];
const strs: Array<string> = ['a', 'b', 'c'];
// 元组(Tuple):固定长度、固定类型的数组
const point: [number, number] = [10, 20];
const entry: [string, number] = ['score', 100];
// 元组解构
const [x, y] = point;
// 可选元组元素(TS 3.0+)
type RGB = [red: number, green: number, blue: number, alpha?: number];
const red: RGB = [255, 0, 0];
const redWithAlpha: RGB = [255, 0, 0, 0.5];
2.3 特殊类型:any、unknown、never、void
这四个类型是 TypeScript 的重点,也是容易踩坑的地方:
// any:关闭类型检查,尽量避免使用
let data: any = '123';
data = 123; // 可以赋值任意类型
data.foo.bar.baz; // 不报错,但运行时可能炸
// unknown:类型安全版的 any,使用前必须做类型检查
let input: unknown = getUserInput();
// input.toUpperCase(); // ❌ 错误!不能直接使用
if (typeof input === 'string') {
input.toUpperCase(); // ✅ 类型收窄后可以使用
}
// void:函数没有返回值时使用
function log(msg: string): void {
console.log(msg);
// 没有 return 语句
}
// never:永远不会有值(函数抛出异常,或死循环)
function throwError(msg: string): never {
throw new Error(msg);
}
function infiniteLoop(): never {
while (true) {}
}
记住:能用 unknown 就不用 any,unknown 是"我不知道是什么类型",但操作前必须确认类型;any 是"我放弃类型检查"。
三、接口(Interface)
接口是 TypeScript 中描述"对象形状"的核心工具。
3.1 基础接口
interface User {
id: number;
name: string;
email: string;
avatar?: string; // 可选属性
readonly createdAt: Date; // 只读属性
}
const user: User = {
id: 1,
name: 'leon',
email: 'leon@example.com',
createdAt: new Date(),
};
// user.createdAt = new Date(); // ❌ 只读属性不能修改
3.2 接口继承
interface BaseEntity {
id: number;
createdAt: Date;
updatedAt: Date;
}
interface User extends BaseEntity {
name: string;
email: string;
}
interface Admin extends User {
role: 'super' | 'normal';
permissions: string[];
}
3.3 函数接口
// 描述函数类型
interface Formatter {
(value: number, locale?: string): string;
}
const formatCurrency: Formatter = (value, locale = 'zh-CN') => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'CNY',
}).format(value);
};
3.4 索引签名
// 当对象键名不确定时使用
interface Dictionary<T> {
[key: string]: T;
}
const scores: Dictionary<number> = {
math: 95,
english: 88,
science: 92,
};
// 混合使用确定属性和索引签名
interface Config {
env: string; // 确定属性
[key: string]: unknown; // 其他任意属性
}
四、类型别名(Type Alias)
type 关键字可以给任何类型起别名,比接口更灵活。
// 基本用法
type ID = string | number;
type Point = { x: number; y: number };
type Callback = (err: Error | null, data?: unknown) => void;
// 联合类型
type Status = 'pending' | 'success' | 'error' | 'loading';
type Theme = 'light' | 'dark';
// 交叉类型:合并多个类型
type WithTimestamp = {
createdAt: Date;
updatedAt: Date;
};
type UserWithTime = User & WithTimestamp;
interface vs type:如何选择?
extends | & | |
经验法则:描述对象的"形状"用 interface,其他(联合类型、工具类型、函数类型等)用 type。
五、函数类型
5.1 函数参数和返回值类型
// 普通函数
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数
const multiply = (a: number, b: number): number => a * b;
// 可选参数和默认值
function greet(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}
// 剩余参数
function sum(...nums: number[]): number {
return nums.reduce((acc, n) => acc + n, 0);
}
5.2 函数重载
当一个函数根据传入参数的不同,返回不同类型时,使用重载:
// 重载签名
function formatDate(date: Date): string;
function formatDate(timestamp: number): string;
function formatDate(dateStr: string): Date;
// 实现签名
function formatDate(input: Date | number | string): string | Date {
if (input instanceof Date) {
return input.toISOString();
}
if (typeof input === 'number') {
return new Date(input).toISOString();
}
return new Date(input);
}
六、泛型(Generics)
泛型是 TypeScript 中最强大的特性之一,它让代码在保持类型安全的同时具有复用性。
6.1 泛型函数
// 不用泛型:丢失类型信息
function identity(arg: any): any {
return arg;
}
// 使用泛型:保留类型信息
function identity<T>(arg: T): T {
return arg;
}
const result1 = identity<string>('hello'); // string 类型
const result2 = identity(42); // TypeScript 自动推断为 number
6.2 泛型约束
// 限制 T 必须有 length 属性
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength('hello'); // ✅ string 有 length
getLength([1, 2, 3]); // ✅ array 有 length
// getLength(123); // ❌ number 没有 length
// 用 keyof 约束键名
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'leon', age: 28 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'email'); // ❌ 'email' 不在 user 中
6.3 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface PageResult<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
// 使用时指定具体类型
type UserListResponse = ApiResponse<PageResult<User>>;
async function fetchUsers(): Promise<UserListResponse> {
const res = await fetch('/api/users');
return res.json();
}
七、枚举(Enum)
// 数字枚举(默认从 0 开始)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 字符串枚举(推荐:便于调试)
enum Status {
Pending = 'PENDING',
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Deleted = 'DELETED',
}
// 使用
function move(dir: Direction): void {
console.log(`Moving ${Direction[dir]}`);
}
move(Direction.Up); // Moving Up
现代 TypeScript 项目中,很多人倾向于用字符串字面量联合类型代替枚举,因为它更轻量:
// 用 type 代替 enum(更简洁)
type Status = 'PENDING' | 'ACTIVE' | 'INACTIVE' | 'DELETED';
// 需要迭代时,配合 as const 使用
const STATUS_VALUES = ['PENDING', 'ACTIVE', 'INACTIVE', 'DELETED'] as const;
type Status = typeof STATUS_VALUES[number];
八、类(Class)与 TypeScript
TypeScript 对 ES6 的 Class 语法做了大量增强。
// 访问修饰符
class Animal {
public name: string; // 公开(默认)
protected age: number; // 子类可访问
private _secret: string; // 仅类内部可访问
readonly species: string; // 只读
constructor(name: string, age: number, species: string) {
this.name = name;
this.age = age;
this._secret = 'hidden';
this.species = species;
}
// 参数属性简写(推荐)
// 上面的写法等同于:
// constructor(
// public name: string,
// protected age: number,
// private _secret: string,
// readonly species: string
// ) {}
}
class Dog extends Animal {
breed: string;
constructor(name: string, age: number, breed: string) {
super(name, age, 'Canis familiaris');
this.breed = breed;
}
// 重写方法
speak(): string {
return `${this.name} barks!`;
// this._secret // ❌ 不可访问 private
// this.age // ✅ 可访问 protected
}
}
// 接口实现
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
class UserModel extends Animal implements Serializable {
email: string;
constructor(name: string, email: string) {
super(name, 0, 'Human');
this.email = email;
}
serialize(): string {
return JSON.stringify({ name: this.name, email: this.email });
}
deserialize(data: string): void {
const obj = JSON.parse(data);
this.name = obj.name;
this.email = obj.email;
}
}
九、React + TypeScript 基础实践
现在把上面学到的内容结合 React 来实践。
9.1 函数组件 Props 类型
import React from 'react';
// 定义 Props 接口
interface ButtonProps {
/** 按钮文本 */
label: string;
/** 按钮类型 */
variant?: 'primary' | 'secondary' | 'danger';
/** 是否禁用 */
disabled?: boolean;
/** 加载中状态 */
loading?: boolean;
/** 点击事件 */
onClick?: () => void;
/** 子元素 */
children?: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
label,
variant = 'primary',
disabled = false,
loading = false,
onClick,
children,
}) => {
return (
<button
className={`btn btn--${variant}`}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? '加载中...' : children || label}
</button>
);
};
export default Button;
9.2 useState 类型
import React, { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC = () => {
// 基础类型推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
// 复杂类型需要泛型指定
const [user, setUser] = useState<User | null>(null);
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState<Error | null>(null);
const fetchUser = async (id: number) => {
try {
const res = await fetch(`/api/users/${id}`);
const data: User = await res.json();
setUser(data);
} catch (err) {
setError(err as Error);
}
};
return (
<div>
{error && <p>{error.message}</p>}
{user && <p>{user.name}</p>}
</div>
);
};
9.3 事件处理类型
React 中的事件类型是很多初学者的困惑点:
import React, { useState } from 'react';
const Form: React.FC = () => {
const [value, setValue] = useState('');
// 表单输入事件
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
// 表单提交事件
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('提交:', value);
};
// 按钮点击事件
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('点击位置:', e.clientX, e.clientY);
};
// 键盘事件
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('回车键');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<button type="submit" onClick={handleClick}>
提交
</button>
</form>
);
};
常用事件类型速查:
React.ChangeEvent<HTMLInputElement> | |
React.ChangeEvent<HTMLSelectElement> | |
React.FormEvent<HTMLFormElement> | |
React.MouseEvent<HTMLButtonElement> | |
React.KeyboardEvent<HTMLInputElement> | |
React.FocusEvent<HTMLInputElement> | |
React.DragEvent<HTMLDivElement> |
9.4 useRef 类型
import React, { useRef, useEffect } from 'react';
const VideoPlayer: React.FC<{ src: string }> = ({ src }) => {
// 引用 DOM 元素:初始值为 null
const videoRef = useRef<HTMLVideoElement>(null);
// 引用可变值(不触发重渲染):初始值确定
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const countRef = useRef<number>(0);
useEffect(() => {
// 访问 DOM 元素需要检查是否为 null
if (videoRef.current) {
videoRef.current.play();
}
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
return <video ref={videoRef} src={src} />;
};
9.5 自定义 Hook 类型
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
// 泛型 Hook,支持任意数据类型
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }));
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data: T = await res.json();
if (!cancelled) {
setState({ data, loading: false, error: null });
}
} catch (err) {
if (!cancelled) {
setState({ data: null, loading: false, error: err as Error });
}
}
};
fetchData();
return () => { cancelled = true; };
}, [url]);
return state;
}
// 使用时享受完整的类型推断
interface Post {
id: number;
title: string;
body: string;
}
const PostDetail: React.FC<{ id: number }> = ({ id }) => {
const { data, loading, error } = useFetch<Post>(`/api/posts/${id}`);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
if (!data) return null;
// data 在这里被推断为 Post 类型,有完整的类型提示
return (
<article>
<h1>{data.title}</h1>
<p>{data.body}</p>
</article>
);
};
十、类型收窄(Type Narrowing)
类型收窄是 TypeScript 中非常实用的特性,它帮助你在运行时确认变量的具体类型。
10.1 typeof 收窄
function padLeft(value: string, padding: string | number): string {
if (typeof padding === 'number') {
// 这里 padding 被收窄为 number
return ' '.repeat(padding) + value;
}
// 这里 padding 被收窄为 string
return padding + value;
}
10.2 instanceof 收窄
function handleError(error: unknown): string {
if (error instanceof Error) {
return error.message; // 收窄为 Error
}
if (typeof error === 'string') {
return error; // 收窄为 string
}
return '未知错误';
}
10.3 in 操作符收窄
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function getArea(shape: Shape): number {
// 使用 kind 字段(判别联合)区分类型
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2; // shape 是 Circle
}
return shape.width * shape.height; // shape 是 Rectangle
}
10.4 类型守卫(Type Guards)
// 自定义类型守卫:返回值是 `arg is Type` 格式
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
function processData(data: unknown): void {
if (isUser(data)) {
console.log(data.name); // data 被收窄为 User
}
}小结
本篇涵盖了 TypeScript 的核心基础:
基础类型 — 原始类型、数组、元组、特殊类型 接口与类型别名 — 描述对象形状,interface vs type 选择 函数类型 — 参数类型、返回值类型、函数重载 泛型 — 让代码在类型安全的前提下复用 枚举 — 及其现代替代方案 类 — 访问修饰符、接口实现 React 基础实践 — Props、useState、事件处理、自定义 Hook 类型收窄 — 运行时类型确认的正确姿势
下篇《TypeScript 从入门到实战(下篇)》将深入讲解:高级类型系统、内置工具类型、复杂泛型技巧、React 状态管理类型化、Context API、API 层类型设计,以及工程化配置。敬请期待!