fp-immutable

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Practical 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 shallow
typescript
// 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 readonly
typescript
// 只读数组
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 about
typescript
// 处理大型数组时,可变操作的性能会显著提升
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

速查表

TaskImmutable Pattern
Add to array end
[...arr, item]
Add to array start
[item, ...arr]
Remove from array
arr.filter(x => x !== item)
Update array item
arr.map(x => x.id === id ? {...x, ...updates} : x)
Update object prop
{...obj, prop: newValue}
Remove object prop
const {prop, ...rest} = obj
Merge objects
{...defaults, ...overrides}
Deep updateUse Immer or nested spread
Prevent mutation
readonly
types or
Object.freeze()
任务不可变模式
向数组末尾添加元素
[...arr, item]
向数组开头添加元素
[item, ...arr]
从数组中删除元素
arr.filter(x => x !== item)
更新数组中的项
arr.map(x => x.id === id ? {...x, ...updates} : x)
更新对象属性
{...obj, prop: newValue}
删除对象属性
const {prop, ...rest} = obj
合并对象
{...defaults, ...overrides}
深层更新使用Immer或嵌套扩展
禁止修改
readonly
类型或
Object.freeze()