要点
TypeScript 在开发时检查代码关系,不会自动验证服务器返回的 JSON。把任意数据断言成 User 只是告诉编译器信任你。
配置和类型系统
- 掌握 Next.js 16中 TypeScript 的完整配置
- 理解 TypeScript 类型系统和类型推断
- 学会编写类型安全的组件和 API
- 掌握泛型编程和高级类型特性
实际应用能力
- 为 Next.js 项目配置 TypeScript 环境
- 编写类型安全的 React 组件
- 处理 API 请求和数据的类型定义
- 使用 TypeScript 工具进行代码质量提升
概述
TypeScript 为 Next.js 应用提供了静态类型检查,大大提升了代码质量和开发体验。Next.js 16对 TypeScript 提供了原生支持,让类型安全的全栈开发变得简单高效。
TypeScript 项目配置
自动配置(推荐)
# 创建项目时自动配置TypeScript
npx create-next-app@latest my-ts-app --typescript --eslint --app
1. 或者手动回答配置问题
npx create-next-app@latest my-ts-app
1. Would you like to use TypeScript? … Yes
手动配置
# 安装TypeScript依赖
npm install --save-dev typescript @types/react @types/node
npm install --save-dev @types/react-dom
1. 创建TypeScript配置文件
npx tsc --init
tsconfig.json 详解
{
"compilerOptions": {
// 目标版本
"target": "ES2017",
// 模块系统
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
// 严格检查
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
// Next.js插件
"plugins": [
{
"name": "next"
}
],
// 路径别名
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/types/*": ["./src/types/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}
类型定义和声明
Next.js 环境类型
// next-env.d.ts (自动生成)
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
全局类型声明
// src/types/global.d.ts
import { NextApiRequest, NextApiResponse } from 'next'
// 扩展全局类型
declare global {
interface Window {
// 浏览器全局变量
gtag?: (...args: any[]) => void
dataLayer?: any[]
}
}
// API类型定义
export type ApiHandler<T = any> = (
req: NextApiRequest,
res: NextApiResponse<T>
) => Promise<void> | void
// 用户类型
export interface User {
id: string
name: string
email: string
avatar?: string
role: 'admin' | 'user' | 'guest'
createdAt: Date
updatedAt: Date
}
// API响应类型
export interface ApiResponse<T = any> {
success: boolean
data?: T
error?: string
message?: string
}
类型安全的组件开发
基础组件类型
// src/types/components.ts
import { ReactNode, ButtonHTMLAttributes, InputHTMLAttributes } from 'react'
// 组件Props基础类型
export interface BaseComponentProps {
className?: string
children?: ReactNode
}
// 按钮组件类型
export interface ButtonProps extends BaseComponentProps {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
onClick?: () => void
}
// 输入框组件类型
export interface InputProps extends BaseComponentProps {
type?: string
placeholder?: string
value?: string
defaultValue?: string
error?: string
required?: boolean
disabled?: boolean
onChange?: (value: string) => void
}
类型安全的组件实现
// src/components/Button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react'
import { cn } from '@/lib/utils'
import { ButtonProps } from '@/types/components'
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({
children,
className,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
onClick,
...props
}, ref) => {
const baseClasses = 'font-medium rounded-lg transition-colors focus:outline-none focus:ring-2'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500'
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
}
return (
<button
ref={ref}
className={cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
(disabled || loading) && 'opacity-50 cursor-not-allowed',
className
)}
disabled={disabled || loading}
onClick={onClick}
{...props}
>
{loading ? (
<span className="inline-flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
加载中...
</span>
) : children}
</button>
)
}
)
Button.displayName = 'Button'
export default Button
泛型组件
// src/components/List.tsx
import { ReactNode } from 'react'
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => ReactNode
keyExtractor: (item: T) => string
emptyMessage?: string
className?: string
}
export function List<T>({
items,
renderItem,
keyExtractor,
emptyMessage = '暂无数据',
className = ''
}: ListProps<T>) {
if (items.length === 0) {
return (
<div className={`text-center py-8 text-gray-500 ${className}`}>
{emptyMessage}
</div>
)
}
return (
<div className={className}>
{items.map((item, index) => (
<div key={keyExtractor(item)}>
{renderItem(item, index)}
</div>
))}
</div>
)
}
// 使用示例
interface User {
id: string
name: string
email: string
}
// 在组件中使用
function UserList({ users }: { users: User[] }) {
return (
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => (
<div className="p-4 border-b">
<h3 className="font-semibold">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</div>
)}
className="divide-y"
/>
)
}
页面和路由的类型安全
App Router 类型定义
// src/types/pages.ts
import { Metadata } from 'next'
// 页面Props类型
export interface PageProps {
params?: Record<string, string>
searchParams?: Record<string, string | string[] | undefined>
}
// 布局Props类型
export interface LayoutProps {
children: ReactNode
params?: Record<string, string>
}
// 元数据生成函数类型
export type MetadataGenerator = (props: PageProps) => Metadata | Promise<Metadata>
类型安全的页面组件
// src/app/posts/[id]/page.tsx
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import { PageProps } from '@/types/pages'
interface Post {
id: string
title: string
content: string
author: string
createdAt: string
}
// 模拟数据获取
async function getPost(id: string): Promise<Post | null> {
// 这里应该是实际的数据库查询
const posts: Record<string, Post> = {
'1': {
id: '1',
title: 'TypeScript入门指南',
content: 'TypeScript是JavaScript的超集...',
author: '张三',
createdAt: '2025-01-01'
}
}
return posts[id] || null
}
// 类型安全的页面组件
export default async function PostPage({ params }: PageProps) {
// 类型断言确保params存在
const { id } = params as { id: string }
const post = await getPost(id)
if (!post) {
notFound()
}
return (
<div className="max-w-4xl mx-auto py-8">
<article className="prose prose-lg">
<h1>{post.title}</h1>
<div className="text-gray-600 mb-4">
作者:{post.author} | 发布时间:{post.createdAt}
</div>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
</div>
)
}
// 生成元数据
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { id } = params as { id: string }
const post = await getPost(id)
return {
title: post?.title || '文章不存在',
description: post?.content?.substring(0, 160) || '文章详情页面',
}
}
API 路由的类型安全
API 类型定义
// src/types/api.ts
import { NextApiRequest, NextApiResponse } from 'next'
// HTTP方法类型
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
// API请求类型
export interface ApiRequest<T = any> extends NextApiRequest {
body?: T
query: Record<string, string | string[]>
params?: Record<string, string>
}
// API响应类型
export interface ApiResponse<T = any> {
success: boolean
data?: T
error?: string
message?: string
timestamp: string
}
// API处理器类型
export type ApiHandler<TRequest = any, TResponse = any> = (
req: ApiRequest<TRequest>,
res: NextApiResponse<ApiResponse<TResponse>>
) => Promise<void> | void
// 中间件类型
export type ApiMiddleware = (
req: ApiRequest,
res: NextApiResponse,
next: () => void
) => Promise<void> | void
类型安全的 API 实现
// src/pages/api/users/index.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { ApiResponse, ApiHandler, User } from '@/types/api'
// 模拟用户数据
const users: User[] = [
{ id: '1', name: '张三', email: 'zhangsan@example.com', avatar: '', role: 'user', createdAt: new Date(), updatedAt: new Date() },
{ id: '2', name: '李四', email: 'lisi@example.com', avatar: '', role: 'admin', createdAt: new Date(), updatedAt: new Date() }
]
// 创建用户请求类型
interface CreateUserRequest {
name: string
email: string
role?: 'user' | 'admin'
}
const handler: ApiHandler<CreateUserRequest, User[]> = async (req, res) => {
try {
switch (req.method) {
case 'GET':
// 获取用户列表
return res.status(200).json({
success: true,
data: users,
timestamp: new Date().toISOString()
})
case 'POST':
// 创建新用户
const { name, email, role = 'user' } = req.body as CreateUserRequest
// 类型验证
if (!name || !email) {
return res.status(400).json({
success: false,
error: '姓名和邮箱是必填项',
timestamp: new Date().toISOString()
})
}
// 检查邮箱是否已存在
if (users.some(user => user.email === email)) {
return res.status(409).json({
success: false,
error: '邮箱已存在',
timestamp: new Date().toISOString()
})
}
// 创建新用户
const newUser: User = {
id: String(users.length + 1),
name,
email,
role,
createdAt: new Date(),
updatedAt: new Date()
}
users.push(newUser)
return res.status(201).json({
success: true,
data: [newUser],
message: '用户创建成功',
timestamp: new Date().toISOString()
})
default:
// 不支持的HTTP方法
return res.status(405).json({
success: false,
error: '不支持的请求方法',
timestamp: new Date().toISOString()
})
}
} catch (error) {
console.error('API错误:', error)
return res.status(500).json({
success: false,
error: '服务器内部错误',
timestamp: new Date().toISOString()
})
}
}
export default handler
客户端类型安全
数据获取类型
// src/hooks/useFetch.ts
import { useState, useEffect } from 'react'
interface FetchState<T> {
data: T | null
loading: boolean
error: string | null
}
export function useFetch<T>(url: string): FetchState<T> & { refetch: () => void } {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null
})
const fetchData = async () => {
setState(prev => ({ ...prev, loading: true, error: null }))
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json() as { success: boolean; data: T; error?: string }
if (result.success) {
setState({
data: result.data,
loading: false,
error: null
})
} else {
setState({
data: null,
loading: false,
error: result.error || '请求失败'
})
}
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error.message : '未知错误'
})
}
}
useEffect(() => {
fetchData()
}, [url])
return {
...state,
refetch: fetchData
}
}
类型安全的状态管理
// src/types/store.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
// 用户状态类型
interface UserState {
user: User | null
isAuthenticated: boolean
loading: boolean
}
// 用户操作类型
interface UserActions {
login: (email: string, password: string) => Promise<void>
logout: () => void
updateUser: (updates: Partial<User>) => void
}
// 组合类型
type UserStore = UserState & UserActions
// 创建类型安全的store
export const useUserStore = create<UserStore>()(
devtools(
persist(
(set, get) => ({
// 初始状态
user: null,
isAuthenticated: false,
loading: false,
// 登录操作
login: async (email: string, password: string) => {
set({ loading: true })
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const result = await response.json()
if (result.success) {
set({
user: result.data,
isAuthenticated: true,
loading: false
})
} else {
throw new Error(result.error)
}
} catch (error) {
set({ loading: false })
throw error
}
},
// 登出操作
logout: () => {
set({
user: null,
isAuthenticated: false,
loading: false
})
},
// 更新用户信息
updateUser: (updates: Partial<User>) => {
const currentUser = get().user
if (currentUser) {
set({
user: { ...currentUser, ...updates, updatedAt: new Date() }
})
}
}
}),
{
name: 'user-store',
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated
})
}
)
)
)
类型测试和验证
类型验证工具
// src/lib/validation.ts
import { z } from 'zod'
// 用户验证schema
export const userSchema = z.object({
id: z.string().min(1),
name: z.string().min(1).max(50),
email: z.string().email(),
avatar: z.string().url().optional(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.date(),
updatedAt: z.date()
})
// 创建用户验证schema
export const createUserSchema = z.object({
name: z.string().min(1, '姓名不能为空').max(50, '姓名不能超过50个字符'),
email: z.string().email('请输入有效的邮箱地址'),
role: z.enum(['admin', 'user']).optional().default('user')
})
// 类型推断
export type CreateUserInput = z.infer<typeof createUserSchema>
export type User = z.infer<typeof userSchema>
// 验证函数
export function validateCreateUser(input: unknown): CreateUserInput {
return createUserSchema.parse(input)
}
// 安全验证函数
export function safeValidateCreateUser(input: unknown): {
success: boolean
data?: CreateUserInput
error?: string
} {
const result = createUserSchema.safeParse(input)
if (result.success) {
return { success: true, data: result.data }
} else {
return {
success: false,
error: result.error.issues.map(e => e.message).join(', ')
}
}
}
最佳实践和优化
类型导入导出
// 使用type-only导入减少运行时依赖
import type { User, ApiResponse } from '@/types'
import { Button } from '@/components'
// 使用命名导出便于树摇
export type { User, ApiResponse }
export { Button, Card }
条件类型和映射类型
// src/types/advanced.ts
// 条件类型
type ApiResult<T> = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T }
// 映射类型
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
type Required<T, K extends keyof T> = T & Required<Pick<T, K>>
// 使用示例
interface UserForm {
name: string
email: string
password: string
}
type UpdateUserForm = Optional<UserForm, 'password'>
type LoginUserForm = Required<UserForm, 'email' | 'password'>
工具类型的使用
// 内置工具类型
interface PartialUser {
id: string
name: string
email: string
role: 'admin' | 'user'
}
// 选择部分属性
type UserBasicInfo = Pick<PartialUser, 'name' | 'email'>
// 排除某些属性
type UserWithoutId = Omit<PartialUser, 'id'>
// 使所有属性可选
type PartialUserInfo = Partial<PartialUser>
// 使所有属性必需
type RequiredUser = Required<PartialUser>
// 属性重命名
type UserWithDisplayName = {
[K in keyof PartialUser as K extends 'name' ? 'displayName' : K]: PartialUser[K]
}
总结
正文验证:未知 JSON 先收窄再使用
下面是本页“类型断言不验证外部 JSON”的最小 TypeScript 契约。验证脚本会直接从此 Markdown 代码块提取内容并以 tsc --strict --noEmit 检查;它验证类型收窄,不构建 Next.js 应用或调用网络。
type Todo = { id: string; title: string; done: boolean };
function isTodo(value: unknown): value is Todo {
if (typeof value !== 'object' || value === null) return false;
const record = value as Record<string, unknown>;
return typeof record.id === 'string'
&& typeof record.title === 'string'
&& typeof record.done === 'boolean';
}
const response: unknown = { id: '1', done: false };
if (isTodo(response)) {
const title: string = response.title;
void title;
} else {
// 缺少 title 的输入不会被断言伪装成 Todo。
const rejected: true = true;
void rejected;
}
通过本教程,你已经掌握了:
- 配置管理: Next.js 16中 TypeScript 的完整配置
- 类型系统: TypeScript 类型系统和类型推断机制
- 组件开发: 类型安全的 React 组件编写
- API 开发: 类型安全的 API 路由实现
- 数据流: 类型安全的数据获取和状态管理
- 验证机制: 使用 Zod 进行运行时类型验证
- 最佳实践: TypeScript 开发中的最佳实践和优化技巧
下一步学习
- 深入学习高级 TypeScript 特性
- 掌握泛型编程和条件类型
- 学习类型守卫和类型断言
- 探索类型级别的编程
- 了解类型系统的性能优化
TypeScript 提高了重构和协作时发现错误的机会,但不会验证浏览器、URL、数据库或第三方服务送来的数据;每个跨边界值仍需要运行时检查。
文档交叉引用
相关文档
参考章节
- 📖 [本模块其他章节]: 环境搭建配置 | 布局路由设计
- 📖 [Knowledge Points 快速参考]: TypeScript 类型参考 | JavaScript 现代语法
总结
项目中使用类型的边界
- 配置服务于明确约束:开启严格检查后先修复真实错误,再逐项理解每个宽松选项的代价;不要为让旧代码通过而整体关闭严格模式或跳过库声明检查。
- 领域类型避免复制:从一个稳定的领域模型推导读取、创建和更新形状,区分服务器生成字段与客户端可写字段。接口和类型别名的选择以表达扩展关系或联合关系为准,不以风格争论代替模型设计。
- Props 表示完整状态空间:为必需、可选和互斥 Props 选择对应的对象或判别联合;
forwardRef的 ref 类型与原生元素或公开实例相符,不能用any掩盖不兼容调用。 - API 类型必须配合运行时契约:请求正文和响应在传输后不带 TypeScript 信息。服务端校验输入、返回稳定错误形状;客户端以状态码和解析结果处理成功、校验失败、未授权与服务故障。
- 高级类型应减少重复而非增加谜语:先写清调用方需要的输入与输出,再选择泛型、映射或条件类型。无法用一个小调用示例解释的类型工具,通常应拆成更具体的类型。
学习成果验收
- [ ] 运行
tsc --noEmit,故意引入一次空值、错误字段或不兼容 Props,确认错误信息能定位到调用点并在修复后消失。 - [ ] 为一个领域对象推导创建与更新 DTO,验证客户端不能写服务端 ID 或时间戳,空更新也会被运行时拒绝。
- [ ] 为组件设计互斥 Props,分别传入合法与冲突组合,确认冲突组合无法通过类型检查。
- [ ] 向 API 发送类型正确但业务非法的输入,以及未知字段,确认运行时验证仍给出明确错误。
- [ ] 为一个泛型工具写两个不同类型调用与预期返回类型,若无法表达,简化为更具体的函数。
贡献与反馈
内容改进
如果你发现本文档有改进空间,欢迎: – 🐛 报告问题: 在 Issues 中提出具体问题 – 💡 建议改进: 提出修改建议和补充内容 – 📝 参与贡献: 提交 PR 完善文档内容
学习反馈
分享你的学习体验: – ✅ 有用内容: 哪些部分对你最有帮助 – ❓ 疑问点: 哪些内容需要进一步澄清 – 🎯 建议: 希望增加什么内容
文档状态: ✅ 已完成最后更新: 2026年9月版本: v1.0.0