TypeScript 快手指南:从入门到小项目实战,30分钟上手

GoRoutine散步
2025-06-25 22:39
阅读 319

作为一名前端开发者,在这5年的工作中,我经历了从jQuery时代到React、Vue再到如今的框架百花齐放,而在这之中最让我感到“如鱼得水”的,就是 TypeScript。

记得在2019年的时候,我们团队第一次尝试引入TypeScript。当时我还在一个电商系统的重构项目中担任主力开发,项目结构已经变得臃肿不堪,JavaScript中大量any类型变量和随意的对象结构让维护成本极高。那会儿,我和同事常常一边debug一边抓头发:这个函数返回的到底是个字符串还是个对象?为什么传过来的数据结构不一致?

是时候来点更靠谱的东西了,我们决定开始转型TypeScript。


从痛苦出发,为何选择TypeScript?

前端性能优化图表-1

从痛苦出发,为何选择TypeScript?

那个项目原本是纯JS写的,页面交互逻辑复杂,接口调用频繁。随着业务迭代,团队成员越来越多,代码风格也开始千奇百怪:

  • 函数参数无校验,有时候传的是id,结果你发现有人传了整个对象;
  • 返回值类型不确定,处理时经常需要额外判断;
  • 调试困难,尤其是多人协作下,很难快速定位错误源头。

这些问题累积下来,导致新功能上线前的回归测试时间越来越长,线上也频频报错。

为了改善这一局面,我们在新一轮迭代中,决定使用TypeScript作为主要开发语言,同时采用渐进式迁移策略——从核心模块开始入手,逐步将关键组件转为TS。


初次接触TypeScript的感受

初次接触TypeScript的感受

刚开始学TypeScript的时候,我也踩了不少坑。最大的障碍不是语法本身,而是思维方式的转变:以前写JS可以随心所欲,现在每个变量都得加上类型,稍有不慎就报错。

比如我当时写了一个简单的表单提交方法:

function handleSubmit(formData: any) {
  api.post('/submit', formData).then(res => {
    console.log(res);
  });
}

看起来没问题吧?但后来我要做验证逻辑的时候,发现根本不知道formData里面有什么字段。于是我把any改成了一个接口:

interface FormData {
  name: string;
  email: string;
  phone?: string; // 可选字段
}

function handleSubmit(formData: FormData) {
  // ...
}

这下好了,编译器会帮我检查是否漏掉了必填字段,甚至在IDE里还能自动补全。


小项目实战:写一个简单的待办事项应用

小项目实战:写一个简单的待办事项应用

与其空谈理论,不如直接来个实操。下面我带大家一起写一个简单的todo应用,用TypeScript + React实现(也可以换成Vue或其他框架)。

项目背景

我们的目标是构建一个具备以下功能的小型应用:

  • 输入框输入待办事项内容
  • 点击添加后列表展示新增项
  • 每一项可标记为完成状态
  • 完成事项支持删除操作

技术选型

  • 框架:React(create-react-app生成)
  • 语言:TypeScript
  • 样式:CSS Modules
  • 状态管理:useState + useReducer(简单场景不需要Redux)

初始化项目

先通过create-react-app创建一个TS模板:

npx create-react-app todo-ts --template typescript
cd todo-ts
npm start

项目跑起来后,你会看到经典的计数器页面。

编写类型定义

首先定义Todo的数据结构:

// types/todo.ts
export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

再定义操作类型Action:

// types/actions.ts
import { Todo } from './todo';

export enum TodoActionTypes {
  ADD = 'ADD_TODO',
  TOGGLE = 'TOGGLE_TODO',
  REMOVE = 'REMOVE_TODO',
}

export type TodoAction =
  | { type: TodoActionTypes.ADD; payload: { text: string } }
  | { type: TodoActionTypes.TOGGLE; payload: { id: number } }
  | { type: TodoActionTypes.REMOVE; payload: { id: number } };

编写reducer管理状态

接下来使用useReducer统一管理todo的状态变化:

// reducer/todoReducer.ts
import { Todo, TodoAction, TodoActionTypes } from '../types';

const initialTodos: Todo[] = [];

export function todoReducer(state: Todo[] = initialTodos, action: TodoAction): Todo[] {
  switch (action.type) {
    case TodoActionTypes.ADD:
      return [
        ...state,
        {
          id: Date.now(),
          text: action.payload.text,
          completed: false,
        },
      ];
    case TodoActionTypes.TOGGLE:
      return state.map(todo =>
        todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
      );
    case TodoActionTypes.REMOVE:
      return state.filter(todo => todo.id !== action.payload.id);
    default:
      return state;
  }
}

主体组件编写

最后是App组件:

// App.tsx
import React, { useState, useReducer } from 'react';
import { todoReducer } from './reducer/todoReducer';
import { Todo, TodoAction, TodoActionTypes } from './types';
import './App.css';

function App() {
  const [inputValue, setInputValue] = useState('');
  const [todos, dispatch] = useReducer(todoReducer, []);

  const handleAdd = () => {
    if (!inputValue.trim()) return;
    dispatch({
      type: TodoActionTypes.ADD,
      payload: {
        text: inputValue.trim(),
      },
    });
    setInputValue('');
  };

  const handleToggle = (id: number) => {
    dispatch({
      type: TodoActionTypes.TOGGLE,
      payload: {
        id,
      },
    });
  };

  const handleRemove = (id: number) => {
    dispatch({
      type: TodoActionTypes.REMOVE,
      payload: {
        id,
      },
    });
  };

  return (
    <div className="App">
      <h2>TypeScript Todo Demo</h2>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Enter new todo..."
      />
      <button onClick={handleAdd}>Add</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : '' }}>
            <span onClick={() => handleToggle(todo.id)}>{todo.text}</span>
            <button onClick={() => handleRemove(todo.id)}>X</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

这样我们就完成了一个完整的todo应用啦!


踩过的坑和解决办法

在实际使用过程中,我也遇到过不少问题,分享几个比较典型的。

坑一:any泛滥问题

刚上TS的时候,很多地方不会写类型,就直接any糊过去。后来查代码时发现一堆any,失去类型检查的意义了。

解决方案:逐步替换。可以先使用unknown替代any,强制自己显式类型检查。

function parseInput(input: unknown) {
  if (typeof input === 'string') {
    // OK
  } else {
    // 错误提示
  }
}

坑二:第三方库没有类型定义

有些旧库在NPM上找不到.d.ts文件,导致TS抱怨。

解决方案:安装对应type包,或者手写类型声明文件,放入src/types/index.d.ts中全局可用。

例如:

declare module 'some-awesome-lib' {
  export default function doSomething(): void;
}

坑三:过度类型推断导致无法扩展

有时候为了偷懒,写了过于具体的类型,后期想扩展却发现各种报错。

经验教训:尽量使用组合类型,而非具体枚举或联合类型,除非必须限制。


上线后的效果总结

这个todo只是一个例子,但在真实项目中,我们迁移完成后,明显感受到以下几个好处:

  1. 错误提前暴露:编译阶段就能发现问题,减少运行时bug
  2. 代码可读性提升:新人更容易理解数据结构和函数行为
  3. 重构更有信心:改函数签名时,IDE能自动提示所有引用位置
  4. 性能影响几乎可以忽略:TS只是开发阶段使用的工具,最终编译成普通JS运行,浏览器兼容性和性能不受影响

给新手几点建议

如果你刚接触TypeScript,别急着全量重构。可以从这几个点下手:

  1. 先写类型,再写逻辑:养成好习惯,把类型想清楚再动键盘
  2. 利用好JSDoc+TS能力:VSCode里Ctrl+鼠标悬停可以查看详细类型信息
  3. 善用联合类型和条件类型:避免冗余的if/else判断
  4. 多看官方文档和社区最佳实践https://www.typescriptlang.org/docs 是宝藏网站
  5. 不要一开始追求完美类型:先把大方向搞对,细节可以慢慢打磨

后记:TypeScript带来的不仅是类型安全

响应式布局概念图-2

TypeScript带给我的不仅仅是“类型安全”,更重要的是一种思维方式上的改变:从随意地“写着玩”变成认真思考每一个变量的边界和用途。

在这个代码即文档的时代,TypeScript已经成为大型前端项目的标配。它就像是一套自带注释的语言,让人与人之间的合作更高效,也让项目走得更远。

如果你还没开始TypeScript之路,真的强烈推荐你尽快迈出第一步。你会发现,写起代码来不仅更稳,心态也会更轻松——因为你知道,TS已经在帮你兜底了。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝