JS的超集------TypeScript
在编程的世界里,有一种Bug总是在你最不期待的时候跳出来,让你抓耳挠腮,怀疑人生。它不是编译时的错误,也不是运行时的异常,而是那种"我明明写对了,为什么就是不工作"的诡异现象。这种现象在JavaScript中尤为常见,因为它是弱类型、动态语言,编译时不会报错,只有运行时才会暴露问题。
从Bug说起
让我们先看看一个简单的JavaScript代码片段:
javascript
function addTodo(title) {
const newTodo = {
id: Date.now(),
title: title,
completed: false
};
setTodos([...todos, newTodo]);
}
这段代码看起来很合理,对吧?但是,如果有人不小心把数字传给了addTodo,比如addTodo(123),会发生什么?在JavaScript中,这不会报错,但会导致title属性变成123,而不是期望的字符串。这种问题只有在运行时才会暴露出来,而这时可能已经造成了严重的后果。
更糟糕的是,当你在大型项目中工作时,这种Bug会像病毒一样蔓延,让你花费大量时间在调试上,而不是写新功能。想象一下,你花了一整天时间调试一个"为什么我的标题显示成数字"的问题,最后发现只是因为有人不小心把addTodo(123)当成了字符串,这种体验简直让人想哭。
TypeScript:为你的代码穿上"防弹衣"
TypeScript就是为了解决这类问题而生的。它不是一种新的语言,而是JavaScript的超集,添加了静态类型系统。这意味着:
- 静态类型:在编写代码时就定义类型,而不是等到运行时
- 边写边检查bug:IDE会在你写代码时就提示可能的错误
- 编译时检查错误:在代码运行前就发现潜在问题
- 代码建议、文档查看都非常方便:类型系统为IDE提供了强大的支持
- 没有未使用的变量等垃圾代码提示:TypeScript会告诉你哪些代码是多余的
想象一下,如果你的代码有"防弹衣",在你写代码的时候就能告诉你"这里可能有问题",而不是等到用户投诉时才发现。这就是TypeScript带来的体验。
TypeScript的类型系统:不只是简单的"字符串"和"数字"
TypeScript的类型系统远比你想象的丰富。让我们来看看几个关键的类型:
TS
// 泛型 类型的传参 T
let arr2:Array<string> = ['a','b','c'];
// 任意类型 可以放弃类型约束
let aa:any = 1;
aa = '11';
aa = true;
aa = {};
aa.hello(); // 任意类型可以调用任何方法
// 未知类型 更安全一些 使用前做类型检测
let bb:unknown = 1;
bb = '11';
bb = {};
// bb.hello(); 会报错 未知类型可以接收任何类型,但是不可以调用任何方法
// 接口 约定对象具有哪些属性和方法
interface User{
name:string;
age:number;
readonly id :number;
hobby?:string;
}
const u:User = {
id:1,
name:'李四',
age:18,
hobby:'篮球',
}
u.name = '李四少爷'; // 可修改
// u.id = 11; // 只读属性 不能修改
// 自定义类型
type ID = string | number;
let num:ID = '234'
type UserType = {
name:string;
age:number;
hobby?:string;
}
- 泛型(Generics) :允许你创建可重用的组件,这些组件可以处理多种类型。例如,
useState<Todo[]>,这里的<Todo[]>就是泛型参数,告诉TypeScript这个状态是Todo对象的数组。 - any vs unknown :
any是TypeScript中的"任何类型",它会关闭类型检查。unknown则是一个更安全的选择,表示"我们不知道这个类型是什么,但它是某种类型"。使用unknown需要在使用前进行类型检查。 - 接口(Interfaces) :用于定义对象的结构。例如,
interface Todo { id: number; title: string; completed: boolean; }。 - type :用于定义类型的别名,可以是基础类型、联合类型或交叉类型。例如,
type ID = number;。
用TypeScript重写Todo应用:从数据模型开始
在开始重写我们的Todo应用之前,我们先要定义数据模型。在TypeScript中,我们使用接口来定义数据结构:
typescript
// 数据模型:Todo 接口
export interface Todo {
id: number;
title: string;
completed: boolean;
}
这个接口定义了Todo对象的结构。在每个需要使用Todo的地方,我们都可以引用这个接口,确保数据的结构正确。这就像给我们的数据加了一个"身份证",确保它始终符合我们的预期。
组件Props的类型约束
在React中,组件的Props是重要的部分。在TypeScript中,我们可以为Props定义明确的类型:
typescript
// 组件Props类型
interface Props {
todos: Todo[];
onToggle: (id: number) => void;
onRemove: (id: number) => void;
}
const TodoList = ({ todos, onToggle, onRemove }: Props) => {
// 组件逻辑
};
这里,Props接口确保了todos是Todo对象的数组,onToggle和onRemove是接收数字并返回void的函数。这避免了在组件中使用错误的类型。
注意 :在React中,React.FC是老版本的写法,现代React推荐使用直接函数声明的写法:
typescript
const TodoList = ({ todos, onToggle, onRemove }: Props) => {
// ...
}
这种写法更简洁,而且TypeScript会自动推断返回类型。
状态管理的类型安全
在React中,我们经常使用useState来管理状态。在TypeScript中,我们可以为状态指定类型:
typescript
const [todos, setTodos] = useState<Todo[]>(() => getStorage<Todo[]>(STORAGE_KEY, []));
这里,<Todo[]>是泛型参数,告诉TypeScript这个状态是Todo对象的数组。这样,当我们使用setTodos时,TypeScript会确保我们传递的是正确的类型。
持久化功能:TypeScript的实现
最后,我们来看看如何使用TypeScript实现持久化功能:
typescript
// 存储工具:处理持久化
export function getStorage<T>(key: string, defaultValue: T): T {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
}
export function setStorage<T>(key: string, value: T): T {
localStorage.setItem(key, JSON.stringify(value));
return value;
}
这些函数使用了泛型,可以处理任何类型的数据。例如,getStorage<Todo[]>(STORAGE_KEY, [])会从localStorage中获取Todo数组,TypeScript会确保我们得到的是正确的类型。
完整代码:TypeScript版本的Todo应用
以下是使用TypeScript重写的Todo应用的完整代码。所有文件都使用了类型安全的写法,确保类型错误在编译时就被捕获。
本文只介绍该项目使用TS编写时的注意事项,不介绍项目思路
数据模型:Todo接口
typescript
export interface Todo {
id: number;
title: string;
completed: boolean;
}
存储工具:处理持久化
typescript
export function getStorage<T>(key: string, defaultValue: T): T {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
}
export function setStorage<T>(key: string, value: T): T {
localStorage.setItem(key, JSON.stringify(value));
return value;
}
自定义Hook:useTodos
typescript
import { useState, useEffect } from 'react';
import { Todo } from './types/todo';
import { getStorage, setStorage } from './utils/storage';
const STORAGE_KEY = 'todos';
export function useTodos() {
const [todos, setTodos] = useState<Todo[]>(() => getStorage<Todo[]>(STORAGE_KEY, []));
const addTodo = (title: string) => {
const newTodo: Todo = {
id: Date.now(),
title,
completed: false,
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const removeTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
useEffect(() => {
setStorage(STORAGE_KEY, todos);
}, [todos]);
return { todos, addTodo, toggleTodo, removeTodo };
}
组件:TodoList
typescript
import React from 'react';
import TodoItem from './TodoItem';
interface Props {
todos: Todo[];
onToggle: (id: number) => void;
onRemove: (id: number) => void;
}
const TodoList = ({ todos, onToggle, onRemove }: Props) => {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onRemove={onRemove}
/>
))}
</ul>
);
};
export default TodoList;
组件:TodoItem
typescript
import React from 'react';
import { Todo } from './types/todo';
interface Props {
todo: Todo;
onToggle: (id: number) => void;
onRemove: (id: number) => void;
}
const TodoItem = ({ todo, onToggle, onRemove }: Props) => {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
<button onClick={() => onRemove(todo.id)}>Delete</button>
</li>
);
};
export default TodoItem;
组件:TodoInput
typescript
import React, { useState } from 'react';
interface Props {
onAdd: (title: string) => void;
}
const TodoInput = ({ onAdd }: Props) => {
const [title, setTitle] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (title.trim()) {
onAdd(title);
setTitle('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
};
export default TodoInput;
主应用:App
typescript
import React from 'react';
import { useTodos } from './hooks/useTodos';
import TodoList from './components/TodoList';
import TodoInput from './components/TodoInput';
export default function App() {
const { todos, addTodo, toggleTodo, removeTodo } = useTodos();
return (
<div className="app">
<h1>Todo List</h1>
<TodoInput onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onRemove={removeTodo}
/>
</div>
);
}
结构:src目录
项目结构如下:

总结
通过TypeScript,我们不仅避免了"神秘"Bug,还让代码更加清晰、可维护。在Todo应用中,我们使用TypeScript定义了数据模型、组件Props、状态和自定义Hook,确保了类型安全。
TypeScript不是一种负担,而是一种提升开发体验的工具。它让你在写代码时就能发现潜在的问题,而不是等到运行时。当你看到IDE在你写代码时就提示"这里可能有问题",你会爱上这种体验。
所以,下次当你想写一个React应用时,为什么不试试TypeScript呢?它可能会改变你对前端开发的看法。毕竟,谁不想在写代码时就避免那些Bug,让开发过程更加顺畅呢?