Next.js 与 TypeScript

要点

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;
}

通过本教程,你已经掌握了:

  1. 配置管理: Next.js 16中 TypeScript 的完整配置
  2. 类型系统: TypeScript 类型系统和类型推断机制
  3. 组件开发: 类型安全的 React 组件编写
  4. API 开发: 类型安全的 API 路由实现
  5. 数据流: 类型安全的数据获取和状态管理
  6. 验证机制: 使用 Zod 进行运行时类型验证
  7. 最佳实践: TypeScript 开发中的最佳实践和优化技巧

下一步学习

  • 深入学习高级 TypeScript 特性
  • 掌握泛型编程和条件类型
  • 学习类型守卫和类型断言
  • 探索类型级别的编程
  • 了解类型系统的性能优化

TypeScript 提高了重构和协作时发现错误的机会,但不会验证浏览器、URL、数据库或第三方服务送来的数据;每个跨边界值仍需要运行时检查。

文档交叉引用

相关文档

参考章节

  • 📖 [本模块其他章节]: 环境搭建配置 | 布局路由设计
  • 📖 [Knowledge Points 快速参考]: TypeScript 类型参考 | JavaScript 现代语法

总结

项目中使用类型的边界

  1. 配置服务于明确约束:开启严格检查后先修复真实错误,再逐项理解每个宽松选项的代价;不要为让旧代码通过而整体关闭严格模式或跳过库声明检查。
  2. 领域类型避免复制:从一个稳定的领域模型推导读取、创建和更新形状,区分服务器生成字段与客户端可写字段。接口和类型别名的选择以表达扩展关系或联合关系为准,不以风格争论代替模型设计。
  3. Props 表示完整状态空间:为必需、可选和互斥 Props 选择对应的对象或判别联合;forwardRef 的 ref 类型与原生元素或公开实例相符,不能用 any 掩盖不兼容调用。
  4. API 类型必须配合运行时契约:请求正文和响应在传输后不带 TypeScript 信息。服务端校验输入、返回稳定错误形状;客户端以状态码和解析结果处理成功、校验失败、未授权与服务故障。
  5. 高级类型应减少重复而非增加谜语:先写清调用方需要的输入与输出,再选择泛型、映射或条件类型。无法用一个小调用示例解释的类型工具,通常应拆成更具体的类型。

学习成果验收

  • [ ] 运行 tsc --noEmit,故意引入一次空值、错误字段或不兼容 Props,确认错误信息能定位到调用点并在修复后消失。
  • [ ] 为一个领域对象推导创建与更新 DTO,验证客户端不能写服务端 ID 或时间戳,空更新也会被运行时拒绝。
  • [ ] 为组件设计互斥 Props,分别传入合法与冲突组合,确认冲突组合无法通过类型检查。
  • [ ] 向 API 发送类型正确但业务非法的输入,以及未知字段,确认运行时验证仍给出明确错误。
  • [ ] 为一个泛型工具写两个不同类型调用与预期返回类型,若无法表达,简化为更具体的函数。

贡献与反馈

内容改进

如果你发现本文档有改进空间,欢迎: – 🐛 报告问题: 在 Issues 中提出具体问题 – 💡 建议改进: 提出修改建议和补充内容 – 📝 参与贡献: 提交 PR 完善文档内容

学习反馈

分享你的学习体验: – ✅ 有用内容: 哪些部分对你最有帮助 – ❓ 疑问点: 哪些内容需要进一步澄清 – 🎯 建议: 希望增加什么内容


文档状态: ✅ 已完成最后更新: 2026年9月版本: v1.0.0

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇