mcpField
F
fp-immutable

fp-immutable

github.com/whatiskadudoing开发1454 次安装39 周前更新
代码仓库

关于 fp-immutable

面向 TypeScript 开发者的实用不可变数据技能,讲解如何通过复制和更新数组、对象及嵌套状态,减少共享状态带来的意外修改,并提供 Immer 与 readonly 类型的用法。

主要功能

  1. 01不可变地更新数组与对象
  2. 02处理嵌套数据更新
  3. 03使用 readonly 类型限制修改
  4. 04借助 Immer 简化复杂更新
  5. 05说明适合局部变更的情形

适用场景

  1. 01更新 React 或 Redux 状态
  2. 02保留旧状态以支持撤销与比较
  3. 03按 ID 修改数组中的项目
  4. 04合并默认配置与覆盖值

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/whatiskadudoing/fp-ts-skills --skill fp-immutable

安装后在 Claude Code 中输入/fp-immutable确认已加载。

详细介绍

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!

实际好处:

  • 调试:保留之前的状态,便于比较
  • 撤销/重做:只需保留旧版本
  • React/Redux:使用 === 检测变化
  • 无副作用:函数不会破坏其他代码

展开语法模式

数组
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]
对象
const user = { name: 'Alice', age: 30, role: 'admin' };

// Update property
const older = { ...user, age: 31 };

// Add property
const withEmail = { ...user, email: '[email protected]' };

// 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 }) };

更新嵌套数据

麻烦之处在于,每一层都需要展开。

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',
      },
    },
  },
};
用于嵌套更新的辅助函数
// 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');

常见任务

切换列表项中的布尔值
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 }, ...]
按 ID 更新数组中的项
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 });
与默认值合并
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' }
克隆并修改
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 },
});

关于 const 的真相

// 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 中的 readonly 类型
// 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

何时可以修改

在带有局部变量的函数内部
// 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;
};
性能关键型代码
// 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
不可变性毫无益处时
// 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;
};

使用 Immer 处理复杂更新

当嵌套展开语法变得繁琐时,可以使用 Immer。

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
在 React useState 中使用 Immer
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;
    });
  };
};

快速参考

任务不可变模式
在数组末尾添加[...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()

评论

支持 Markdown