跳至主内容区

使用 TypeScript 或 Flow

[非官方测试版翻译]

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

egghead.io lesson 12: Immer + TypeScript

Immer 包内置了类型定义,TypeScript 和 Flow 开箱即用,无需额外配置即可自动识别。

TypeScript 类型系统会自动移除草稿类型中的 readonly 修饰符,并返回与原始类型匹配的值。参考以下示例:

import {produce} from "immer"

interface State {
readonly x: number
}

// `x` cannot be modified here
const state: State = {
x: 0
}

const newState = produce(state, draft => {
// `x` can be modified here
draft.x++
})

// `newState.x` cannot be modified here

这确保了状态修改只能发生在 produce 回调函数中。该机制支持递归处理,并能兼容 ReadonlyArray 类型。

最佳实践

  1. 尽可能将状态定义为 readonly。这最符合实际心智模型,因为 Immer 会冻结所有返回值。

  2. 可使用工具类型 Immutable 递归实现整个类型树的只读化,例如:type ReadonlyState = Immutable<State>

  3. 若输入状态的原始类型非不可变,Immer 不会自动将返回类型包裹在 Immutable 中,以避免破坏未使用不可变类型的代码库。

柯里化生成器技巧

我们尽可能进行类型推断。若柯里化生成器被创建后直接传递给其他函数(如 React),类型可自动推断:

import {Immutable, produce} from "immer"

type Todo = Immutable<{
title: string
done: boolean
}>

// later...

const [todo, setTodo] = useState<Todo>({
title: "test",
done: true
})

// later...

setTodo(
produce(draft => {
// draft will be strongly typed and mutable!
draft.done = !draft.done
})
)

当柯里化生成器未直接传递时,Immer 可通过草稿参数推断状态类型。例如:

// See below for a better solution!

const toggler = produce((draft: Draft<Todo>) => {
draft.done = !draft.done
})

// typeof toggler = (state: Immutable<Todo>) => Writable<Todo>

注意:我们确实将 draft 参数的 Todo 类型用 Draft 包裹,因为 Todo 是只读类型。对于非只读类型,则无需此操作。

对于返回的柯里化函数 toggler,我们会 收窄 输入 类型为 Immutable<Todo>。因此即使 Todo 是可变类型,仍可接受不可变 todo 作为输入参数给 toggler

相反地,Immer 会拓宽柯里化函数的输出类型为 Writable<Todo>,确保输出状态可分配给未显式声明为不可变的变量。

这种类型收窄/拓宽行为可能不受欢迎(特别是会导致类型声明冗长)。建议在无法直接推断时显式指定柯里化生成器的泛型状态类型(如上例中的 toggler)。这样会跳过自动的输入收窄/输出拓宽,但 draft 参数仍会被推断为可写的 Draft<Todo>

const toggler = produce<Todo>(draft => {
draft.done = !draft.done
})

// typeof toggler = (state: Todo) => Todo

若柯里化生成器定义了初始状态,Immer 可直接从初始状态推断类型,此时也无需指定泛型:

const state0: Todo = {
title: "test",
done: false
}

// No type annotations needed, since we can infer from state0.
const toggler = produce(draft => {
draft.done = !draft.done
}, state0)

// typeof toggler = (state: Todo) => Todo

当 toggler 无初始状态但存在柯里化参数,且需显式设置状态泛型时,额外参数的类型应显式定义为元组类型:

const toggler = produce<Todo, [boolean]>((draft, newState) => {
draft.done = newState
})

// typeof toggler = (state: Todo, newState: boolean) => Todo

类型转换工具

produce 内外类型概念相同但实践视角不同。例如前文中的 Stateproduce 外部应视为不可变,在 produce 内部则为可变。

有时会导致实际冲突,参考以下示例:

type Todo = {readonly done: boolean}

type State = {
readonly finishedTodos: readonly Todo[]
readonly unfinishedTodos: readonly Todo[]
}

function markAllFinished(state: State) {
produce(state, draft => {
draft.finishedTodos = state.unfinishedTodos
})
}

将触发错误:

The type 'readonly Todo[]' is 'readonly' and cannot be assigned to the mutable type '{ done: boolean; }[]'

错误原因:我们将只读的不可变数组赋给了需要可变类型(含 .push 等方法)的草稿。TypeScript 认为原始 State 未暴露这些方法。使用工具 castDraft 可提示 TS 将集合上转为可变数组:

执行 draft.finishedTodos = castDraft(state.unfinishedTodos) 即可消除错误。

另外还提供了 castImmutable 工具函数,用于实现相反的效果。请注意,这些工具函数在实际操作中都是无副作用的,它们仅返回原始值。

提示:你可以将 castImmutableproduce 结合使用,从而将 produce 的返回类型显式指定为不可变类型,即使原始状态是可变的:

// a mutable data structure
const baseState = {
todos: [{
done: false
}]
}

const nextState = castImmutable(produce(baseState, _draft => {}))

// inferred type of nextState is now:
{
readonly todos: ReadonlyArray<{
readonly done: boolean
}>
})

兼容性

注意: Immer v5.3+ 仅支持 TypeScript v3.7+ 及以上版本。

注意: Immer v3.0+ 仅支持 TypeScript v3.4+ 及以上版本。

注意: Immer v1.9+ 仅支持 TypeScript v3.1+ 及以上版本。

注意: Flow 支持可能会在未来的版本中移除,我们推荐使用 TypeScript