fp-immutable
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePractical Immutability in TypeScript
TypeScript中的实用不可变编程
Why Immutability Helps
不可变编程的优势
typescript
// Bug: shared state causes unexpected behavior
const filters = { active: true, category: 'all' };
const savedFilters = filters; // Not a copy!
filters.active = false;
console.log(savedFilters.active); // false - oops!
// Fix: immutable update creates a new object
const filters2 = { active: true, category: 'all' };
const savedFilters2 = { ...filters2 }; // Actual copy
filters2.active = false;
console.log(savedFilters2.active); // true - safe!Benefits in practice:
- Debugging: Previous state preserved, easy to compare
- Undo/redo: Just keep old versions
- React/Redux: Change detection with
=== - No side effects: Functions don't break other code
typescript
// 问题:共享状态导致意外行为
const filters = { active: true, category: 'all' };
const savedFilters = filters; // 这不是拷贝!
filters.active = false;
console.log(savedFilters.active); // false - 糟糕!
// 修复方案:不可变更新创建新对象
const filters2 = { active: true, category: 'all' };
const savedFilters2 = { ...filters2 }; // 真正的拷贝
filters2.active = false;
console.log(savedFilters2.active); // true - 安全!实际开发中的优势:
- 调试:保留历史状态,便于对比
- 撤销/重做:只需保留旧版本
- React/Redux:通过检测变化
=== - 无副作用:函数不会破坏其他代码
Spread Patterns
扩展运算符模式
Arrays
数组
typescript
const items = [1, 2, 3];
// Add to end
const added = [...items, 4]; // [1, 2, 3, 4]
// Add to start
const prepended = [0, ...items]; // [0, 1, 2, 3]
// Insert at index
const inserted = [...items.slice(0, 1), 99, ...items.slice(1)]; // [1, 99, 2, 3]
// Remove by index
const removed = [...items.slice(0, 1), ...items.slice(2)]; // [1, 3]
// Remove by value
const filtered = items.filter(x => x !== 2); // [1, 3]
// Update by index
const updated = items.map((x, i) => i === 1 ? 99 : x); // [1, 99, 3]
// Replace matching items
const replaced = items.map(x => x === 2 ? 99 : x); // [1, 99, 3]typescript
const items = [1, 2, 3];
// 添加到末尾
const added = [...items, 4]; // [1, 2, 3, 4]
// 添加到开头
const prepended = [0, ...items]; // [0, 1, 2, 3]
// 插入到指定索引
const inserted = [...items.slice(0, 1), 99, ...items.slice(1)]; // [1, 99, 2, 3]
// 按索引删除
const removed = [...items.slice(0, 1), ...items.slice(2)]; // [1, 3]
// 按值删除
const filtered = items.filter(x => x !== 2); // [1, 3]
// 按索引更新
const updated = items.map((x, i) => i === 1 ? 99 : x); // [1, 99, 3]
// 替换匹配项
const replaced = items.map(x => x === 2 ? 99 : x); // [1, 99, 3]Objects
对象
typescript
const user = { name: 'Alice', age: 30, role: 'admin' };
// Update property
const older = { ...user, age: 31 };
// Add property
const withEmail = { ...user, email: 'alice@example.com' };
// Remove property (destructure + spread)
const { role, ...withoutRole } = user; // { name: 'Alice', age: 30 }
// Merge objects (later wins)
const defaults = { theme: 'light', lang: 'en' };
const prefs = { theme: 'dark' };
const merged = { ...defaults, ...prefs }; // { theme: 'dark', lang: 'en' }
// Conditional update
const maybeUpdate = { ...user, ...(shouldUpdate && { age: 31 }) };typescript
const user = { name: 'Alice', age: 30, role: 'admin' };
// 更新属性
const older = { ...user, age: 31 };
// 添加属性
const withEmail = { ...user, email: 'alice@example.com' };
// 删除属性(解构 + 扩展)
const { role, ...withoutRole } = user; // { name: 'Alice', age: 30 }
// 合并对象(后面的属性会覆盖前面的)
const defaults = { theme: 'light', lang: 'en' };
const prefs = { theme: 'dark' };
const merged = { ...defaults, ...prefs }; // { theme: 'dark', lang: 'en' }
// 条件更新
const maybeUpdate = { ...user, ...(shouldUpdate && { age: 31 }) };Updating Nested Data
嵌套数据更新
The annoying part - every level needs spreading.
typescript
interface State {
user: {
profile: {
name: string;
settings: {
theme: string;
notifications: boolean;
};
};
};
}
const state: State = {
user: {
profile: {
name: 'Alice',
settings: {
theme: 'light',
notifications: true,
},
},
},
};
// Update deeply nested value
const updated: State = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
settings: {
...state.user.profile.settings,
theme: 'dark',
},
},
},
};这是麻烦的部分——每一层都需要使用扩展运算符。
typescript
interface State {
user: {
profile: {
name: string;
settings: {
theme: string;
notifications: boolean;
};
};
};
}
const state: State = {
user: {
profile: {
name: 'Alice',
settings: {
theme: 'light',
notifications: true,
},
},
},
};
// 更新深层嵌套的值
const updated: State = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
settings: {
...state.user.profile.settings,
theme: 'dark',
},
},
},
};Helper Function for Nested Updates
嵌套更新的辅助函数
typescript
// Simple lens-like helper
const updateIn = <T>(
obj: T,
path: string[],
updater: (val: any) => any
): T => {
if (path.length === 0) return updater(obj) as T;
const [key, ...rest] = path;
return {
...obj,
[key]: updateIn((obj as any)[key], rest, updater),
} as T;
};
// Usage
const updated2 = updateIn(state, ['user', 'profile', 'settings', 'theme'], () => 'dark');typescript
// 类似透镜的简单辅助函数
const updateIn = <T>(
obj: T,
path: string[],
updater: (val: any) => any
): T => {
if (path.length === 0) return updater(obj) as T;
const [key, ...rest] = path;
return {
...obj,
[key]: updateIn((obj as any)[key], rest, updater),
} as T;
};
// 使用示例
const updated2 = updateIn(state, ['user', 'profile', 'settings', 'theme'], () => 'dark');Common Tasks
常见任务
Toggle Boolean in a List Item
切换列表项中的布尔值
typescript
interface Todo {
id: number;
text: string;
done: boolean;
}
const todos: Todo[] = [
{ id: 1, text: 'Learn FP', done: false },
{ id: 2, text: 'Use immutability', done: false },
];
// Toggle by ID
const toggleTodo = (todos: Todo[], id: number): Todo[] =>
todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
);
const toggled = toggleTodo(todos, 1);
// [{ id: 1, text: 'Learn FP', done: true }, ...]typescript
interface Todo {
id: number;
text: string;
done: boolean;
}
const todos: Todo[] = [
{ id: 1, text: '学习函数式编程', done: false },
{ id: 2, text: '使用不可变编程', done: false },
];
// 按ID切换状态
const toggleTodo = (todos: Todo[], id: number): Todo[] =>
todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
);
const toggled = toggleTodo(todos, 1);
// [{ id: 1, text: '学习函数式编程', done: true }, ...]Update Item in Array by ID
按ID更新数组中的项
typescript
interface User {
id: number;
name: string;
score: number;
}
const users: User[] = [
{ id: 1, name: 'Alice', score: 100 },
{ id: 2, name: 'Bob', score: 85 },
];
// Update specific user
const updateUser = (
users: User[],
id: number,
updates: Partial<User>
): User[] =>
users.map(user =>
user.id === id ? { ...user, ...updates } : user
);
const updated3 = updateUser(users, 1, { score: 110 });typescript
interface User {
id: number;
name: string;
score: number;
}
const users: User[] = [
{ id: 1, name: 'Alice', score: 100 },
{ id: 2, name: 'Bob', score: 85 },
];
// 更新指定用户
const updateUser = (
users: User[],
id: number,
updates: Partial<User>
): User[] =>
users.map(user =>
user.id === id ? { ...user, ...updates } : user
);
const updated3 = updateUser(users, 1, { score: 110 });Merge with Defaults
与默认值合并
typescript
interface Config {
timeout: number;
retries: number;
baseUrl: string;
}
const defaults: Config = {
timeout: 5000,
retries: 3,
baseUrl: 'https://api.example.com',
};
const createConfig = (overrides: Partial<Config>): Config => ({
...defaults,
...overrides,
});
const config = createConfig({ timeout: 10000 });
// { timeout: 10000, retries: 3, baseUrl: 'https://api.example.com' }typescript
interface Config {
timeout: number;
retries: number;
baseUrl: string;
}
const defaults: Config = {
timeout: 5000,
retries: 3,
baseUrl: 'https://api.example.com',
};
const createConfig = (overrides: Partial<Config>): Config => ({
...defaults,
...overrides,
});
const config = createConfig({ timeout: 10000 });
// { timeout: 10000, retries: 3, baseUrl: 'https://api.example.com' }Clone with Modifications
克隆并修改
typescript
interface Order {
id: string;
items: string[];
status: 'pending' | 'shipped' | 'delivered';
metadata: Record<string, string>;
}
const cloneOrder = (order: Order, modifications: Partial<Order>): Order => ({
...order,
...modifications,
// Deep clone arrays and objects if needed
items: modifications.items ?? [...order.items],
metadata: { ...order.metadata, ...modifications.metadata },
});typescript
interface Order {
id: string;
items: string[];
status: 'pending' | 'shipped' | 'delivered';
metadata: Record<string, string>;
}
const cloneOrder = (order: Order, modifications: Partial<Order>): Order => ({
...order,
...modifications,
// 若需要则深度克隆数组和对象
items: modifications.items ?? [...order.items],
metadata: { ...order.metadata, ...modifications.metadata },
});The Truth About const
关于const的真相
typescript
// const prevents REASSIGNMENT, not MUTATION
const arr = [1, 2, 3];
arr.push(4); // Works! arr is now [1, 2, 3, 4]
arr[0] = 99; // Works! arr is now [99, 2, 3, 4]
// arr = [5, 6, 7]; // Error: Cannot assign to 'arr'
const obj = { name: 'Alice' };
obj.name = 'Bob'; // Works! obj is now { name: 'Bob' }
// obj = {}; // Error: Cannot assign to 'obj'
// Use readonly types for compile-time immutability
const readonlyArr: readonly number[] = [1, 2, 3];
// readonlyArr.push(4); // Error: Property 'push' does not exist
// readonlyArr[0] = 99; // Error: Index signature only permits reading
const readonlyObj: Readonly<{ name: string }> = { name: 'Alice' };
// readonlyObj.name = 'Bob'; // Error: Cannot assign to 'name'
// Object.freeze() for runtime immutability (shallow!)
const frozen = Object.freeze({ name: 'Alice', nested: { value: 1 } });
// frozen.name = 'Bob'; // Silently fails (or throws in strict mode)
frozen.nested.value = 2; // Works! freeze is shallowtypescript
// const禁止的是重新赋值,而非修改
const arr = [1, 2, 3];
arr.push(4); // 可以执行!arr现在是[1,2,3,4]
arr[0] = 99; // 可以执行!arr现在是[99,2,3,4]
// arr = [5,6,7]; // 错误:无法赋值给'arr'
const obj = { name: 'Alice' };
obj.name = 'Bob'; // 可以执行!obj现在是{ name: 'Bob' }
// obj = {}; // 错误:无法赋值给'obj'
// 使用readonly类型实现编译时不可变
const readonlyArr: readonly number[] = [1,2,3];
// readonlyArr.push(4); // 错误:不存在属性'push'
// readonlyArr[0] = 99; // 错误:索引签名仅允许读取
const readonlyObj: Readonly<{ name: string }> = { name: 'Alice' };
// readonlyObj.name = 'Bob'; // 错误:无法赋值给'name'
// Object.freeze()实现运行时不可变(仅浅层!)
const frozen = Object.freeze({ name: 'Alice', nested: { value: 1 } });
// frozen.name = 'Bob'; // 静默失败(严格模式下会抛出错误)
frozen.nested.value = 2; // 可以执行!freeze是浅层的readonly Types in TypeScript
TypeScript中的readonly类型
typescript
// Readonly array
type ImmutableList<T> = readonly T[];
// Readonly object
type ImmutableUser = Readonly<{
name: string;
age: number;
}>;
// Deep readonly (recursive)
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Usage
interface State {
users: Array<{ name: string; active: boolean }>;
settings: { theme: string };
}
type ImmutableState = DeepReadonly<State>;
// All nested properties are now readonlytypescript
// 只读数组
type ImmutableList<T> = readonly T[];
// 只读对象
type ImmutableUser = Readonly<{
name: string;
age: number;
}>;
// 深层只读(递归)
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// 使用示例
interface State {
users: Array<{ name: string; active: boolean }>;
settings: { theme: string };
}
type ImmutableState = DeepReadonly<State>;
// 所有嵌套属性现在都是只读的When Mutation Is OK
何时可以使用可变操作
Inside Functions with Local Variables
函数内部的局部变量
typescript
// Mutation inside function is fine - no external state affected
const sumSquares = (nums: number[]): number => {
let total = 0; // Local mutable variable
for (const n of nums) {
total += n * n; // Mutation is fine here
}
return total;
};
// Building up a result locally
const groupBy = <T, K extends string>(
items: T[],
keyFn: (item: T) => K
): Record<K, T[]> => {
const result: Record<string, T[]> = {}; // Local mutation
for (const item of items) {
const key = keyFn(item);
if (!result[key]) result[key] = [];
result[key].push(item); // Mutating local object
}
return result;
};typescript
// 函数内部的可变操作是安全的——不会影响外部状态
const sumSquares = (nums: number[]): number => {
let total = 0; // 局部可变变量
for (const n of nums) {
total += n * n; // 这里的可变操作是可行的
}
return total;
};
// 局部构建结果
const groupBy = <T, K extends string>(
items: T[],
keyFn: (item: T) => K
): Record<K, T[]> => {
const result: Record<string, T[]> = {}; // 局部可变对象
for (const item of items) {
const key = keyFn(item);
if (!result[key]) result[key] = [];
result[key].push(item); // 修改局部对象
}
return result;
};Performance-Critical Code
性能敏感的代码
typescript
// When processing large arrays, mutation can be significantly faster
const processLargeArray = (items: number[]): number[] => {
// Creating new arrays in a hot loop = GC pressure
// Mutation here is a valid optimization
const result = new Array(items.length);
for (let i = 0; i < items.length; i++) {
result[i] = items[i] * 2;
}
return result;
};
// But profile first! Often the difference doesn't matter
// and immutable code is easier to reason abouttypescript
// 处理大型数组时,可变操作的性能会显著提升
const processLargeArray = (items: number[]): number[] => {
// 在热循环中创建新数组会增加GC压力
// 这里使用可变操作是合理的优化
const result = new Array(items.length);
for (let i = 0; i < items.length; i++) {
result[i] = items[i] * 2;
}
return result;
};
// 但请先做性能分析!通常性能差异不明显
// 而且不可变代码更易于理解When Immutability Adds No Value
不可变编程无价值的场景
typescript
// Single-use object being built up
const buildConfig = () => {
const config: Record<string, unknown> = {};
config.env = process.env.NODE_ENV;
config.debug = process.env.DEBUG === 'true';
config.port = parseInt(process.env.PORT || '3000');
return Object.freeze(config); // Freeze before returning
};
// Local array being populated
const fetchAllPages = async <T>(fetchPage: (n: number) => Promise<T[]>): Promise<T[]> => {
const allItems: T[] = [];
let page = 1;
while (true) {
const items = await fetchPage(page);
if (items.length === 0) break;
allItems.push(...items); // Local mutation is fine
page++;
}
return allItems;
};typescript
// 一次性构建的对象
const buildConfig = () => {
const config: Record<string, unknown> = {};
config.env = process.env.NODE_ENV;
config.debug = process.env.DEBUG === 'true';
config.port = parseInt(process.env.PORT || '3000');
return Object.freeze(config); // 返回前冻结
};
// 局部填充数组
const fetchAllPages = async <T>(fetchPage: (n: number) => Promise<T[]>): Promise<T[]> => {
const allItems: T[] = [];
let page = 1;
while (true) {
const items = await fetchPage(page);
if (items.length === 0) break;
allItems.push(...items); // 局部可变操作是可行的
page++;
}
return allItems;
};Immer for Complex Updates
使用Immer处理复杂更新
When spread nesting gets painful, use Immer.
typescript
import { produce } from 'immer';
interface State {
users: Array<{
id: number;
name: string;
posts: Array<{
id: number;
title: string;
likes: number;
}>;
}>;
}
const state: State = {
users: [
{
id: 1,
name: 'Alice',
posts: [
{ id: 1, title: 'Hello', likes: 5 },
{ id: 2, title: 'World', likes: 3 },
],
},
],
};
// Without Immer - nested spread nightmare
const withoutImmer: State = {
...state,
users: state.users.map(user =>
user.id === 1
? {
...user,
posts: user.posts.map(post =>
post.id === 1 ? { ...post, likes: post.likes + 1 } : post
),
}
: user
),
};
// With Immer - write mutations, get immutability
const withImmer = produce(state, draft => {
const user = draft.users.find(u => u.id === 1);
if (user) {
const post = user.posts.find(p => p.id === 1);
if (post) {
post.likes += 1; // Looks like mutation, but it's safe!
}
}
});
// Both produce the same immutable result当嵌套扩展变得繁琐时,可以使用Immer。
typescript
import { produce } from 'immer';
interface State {
users: Array<{
id: number;
name: string;
posts: Array<{
id: number;
title: string;
likes: number;
}>;
}>;
}
const state: State = {
users: [
{
id: 1,
name: 'Alice',
posts: [
{ id: 1, title: 'Hello', likes: 5 },
{ id: 2, title: 'World', likes: 3 },
],
},
],
};
// 不使用Immer——嵌套扩展的噩梦
const withoutImmer: State = {
...state,
users: state.users.map(user =>
user.id === 1
? {
...user,
posts: user.posts.map(post =>
post.id === 1 ? { ...post, likes: post.likes + 1 } : post
),
}
: user
),
};
// 使用Immer——编写可变代码,获得不可变结果
const withImmer = produce(state, draft => {
const user = draft.users.find(u => u.id === 1);
if (user) {
const post = user.posts.find(p => p.id === 1);
if (post) {
post.likes += 1; // 看起来是可变操作,但实际是安全的!
}
}
});
// 两种方式都会生成相同的不可变结果Immer with React useState
在React useState中使用Immer
typescript
import { useImmer } from 'use-immer';
interface FormState {
user: { name: string; email: string };
preferences: { theme: string; notifications: boolean };
}
const MyComponent = () => {
const [state, updateState] = useImmer<FormState>({
user: { name: '', email: '' },
preferences: { theme: 'light', notifications: true },
});
const updateName = (name: string) => {
updateState(draft => {
draft.user.name = name;
});
};
const toggleNotifications = () => {
updateState(draft => {
draft.preferences.notifications = !draft.preferences.notifications;
});
};
};typescript
import { useImmer } from 'use-immer';
interface FormState {
user: { name: string; email: string };
preferences: { theme: string; notifications: boolean };
}
const MyComponent = () => {
const [state, updateState] = useImmer<FormState>({
user: { name: '', email: '' },
preferences: { theme: 'light', notifications: true },
});
const updateName = (name: string) => {
updateState(draft => {
draft.user.name = name;
});
};
const toggleNotifications = () => {
updateState(draft => {
draft.preferences.notifications = !draft.preferences.notifications;
});
};
};Quick Reference
速查表
| Task | Immutable Pattern |
|---|---|
| Add to array end | |
| Add to array start | |
| Remove from array | |
| Update array item | |
| Update object prop | |
| Remove object prop | |
| Merge objects | |
| Deep update | Use Immer or nested spread |
| Prevent mutation | |
| 任务 | 不可变模式 |
|---|---|
| 向数组末尾添加元素 | |
| 向数组开头添加元素 | |
| 从数组中删除元素 | |
| 更新数组中的项 | |
| 更新对象属性 | |
| 删除对象属性 | |
| 合并对象 | |
| 深层更新 | 使用Immer或嵌套扩展 |
| 禁止修改 | |