×

TypeScript 从入门到实战(上篇):打牢基础,告别 any

独孤求败 独孤求败 发表于2026-06-09 16:48:22 浏览11 评论0

抢沙发发表评论

本文是《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 就不用 anyunknown 是"我不知道是什么类型",但操作前必须确认类型;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:如何选择?

特性
interface
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>
  );
};

常用事件类型速查

场景
类型
input/textarea 输入
React.ChangeEvent<HTMLInputElement>
select 变化
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 的核心基础:

  1. 基础类型 — 原始类型、数组、元组、特殊类型
  2. 接口与类型别名 — 描述对象形状,interface vs type 选择
  3. 函数类型 — 参数类型、返回值类型、函数重载
  4. 泛型 — 让代码在类型安全的前提下复用
  5. 枚举 — 及其现代替代方案
  6.  — 访问修饰符、接口实现
  7. React 基础实践 — Props、useState、事件处理、自定义 Hook
  8. 类型收窄 — 运行时类型确认的正确姿势

下篇《TypeScript 从入门到实战(下篇)》将深入讲解:高级类型系统、内置工具类型、复杂泛型技巧、React 状态管理类型化、Context API、API 层类型设计,以及工程化配置。敬请期待!


群贤毕至

访客