Doorgaan naar hoofdinhoud

TypeScript of Flow gebruiken

[Onofficiële Beta-vertaling]

Deze pagina is vertaald door PageTurner AI (beta). Niet officieel goedgekeurd door het project. Een fout gevonden? Probleem melden →

egghead.io lesson 12: Immer + TypeScript

Het Immer-pakket bevat type-definities in het pakket zelf, die door TypeScript en Flow zonder verdere configuratie direct herkend moeten worden.

De TypeScript-typings verwijderen automatisch readonly-modifiers van je draft-types en retourneren een waarde die overeenkomt met je oorspronkelijke type. Bekijk dit praktische voorbeeld:

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

Dit garandeert dat je state alleen binnen je produce-callbacks gewijzigd kan worden. Dit werkt zelfs recursief en met ReadonlyArray.

Best practices​

  1. Definieer je states zoveel mogelijk als readonly. Dit sluit het beste aan bij het mentale model en de realiteit, aangezien Immer alle geretourneerde waarden bevriest.

  2. Gebruik het utility-type Immutable om een volledige type-structuur recursief read-only te maken, bijv.: type ReadonlyState = Immutable<State>.

  3. Immer zal niet automatisch alle geretourneerde typen in Immutable verpakken als het oorspronkelijke input-type niet immutable was. Dit voorkomt problemen met codebases die geen immutable typen gebruiken.

Tips voor curried producers​

We proberen zoveel mogelijk type-inferentie toe te passen. Als een curried producer direct aan een andere functie wordt doorgegeven, kunnen we het type daaruit afleiden. Dit werkt bijvoorbeeld goed met 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
})
)

Wanneer een curried producer niet direct wordt doorgegeven, kan Immer het state-type afleiden uit het draft-argument. Bijvoorbeeld:

// See below for a better solution!

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

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

Let op: we hebben het Todo-type van het draft-argument verpakt in Draft omdat Todo een readonly-type is. Voor niet-readonly typen is dit niet nodig.

Voor de geretourneerde curried functie toggler zullen we het input-type verengen naar Immutable<Todo>, zodat we ondanks het mutable Todo-type een immutable todo als invoer voor toggler accepteren.

Immer zal daarentegen het output-type van de curried functie verbreden naar Writable<Todo>, zodat de uitvoerstate ook toewijsbaar is aan variabelen zonder expliciete immutable typen.

Dit type-verengend/verbredend gedrag kan ongewenst zijn, bijvoorbeeld omdat het rommelige typen oplevert. We raden daarom aan het generieke state-type expliciet te specificeren bij curried producers waar inferentie niet direct werkt, zoals bij toggler hierboven. Hierdoor wordt automatische output-verbreding/input-verenging overgeslagen. Het draft-argument blijft wel een schrijfbare Draft<Todo>:

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

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

Als een curried producer echter met een initiële state is gedefinieerd, kan Immer het state-type daaruit afleiden en is het generieke type overbodig:

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

Als de toggler geen initiële state heeft, extra curried argumenten bevat, en je het state-generieke type expliciet instelt, moet je extra argumenttypen expliciet als tuple-type definiëren:

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

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

Cast-hulpmiddelen​

Typen binnen en buiten produce zijn conceptueel gelijk maar praktisch verschillend. De State uit eerdere voorbeelden is bijvoorbeeld buiten produce immutable, maar binnen produce mutable.

Dit leidt soms tot praktische conflicten. Neem dit voorbeeld:

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

Dit genereert de fout:

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

Deze fout ontstaat omdat we een read-only immutable array toewijzen aan onze draft, die een mutable type verwacht met methoden zoals .push etc. TypeScript ziet deze methoden niet in het oorspronkelijke State. Gebruik de castDraft utility om aan te geven dat we de collectie voor draft-doeleinden naar een mutable array willen upcasten:

draft.finishedTodos = castDraft(state.unfinishedTodos) laat de fout verdwijnen.

Er is ook de utility castImmutable voor wanneer je het tegenovergestelde moet bereiken. Let op: deze utilities zijn in de praktijk no-ops - ze retourneren simpelweg hun oorspronkelijke waarde.

Tip: Je kunt castImmutable combineren met produce om het retourtype van produce als onveranderbaar te typen, zelfs wanneer de oorspronkelijke staat veranderbaar was:

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

Compatibiliteit​

Opmerking: Immer v5.3+ ondersteunt alleen TypeScript v3.7+.

Opmerking: Immer v3.0+ ondersteunt alleen TypeScript v3.4+.

Opmerking: Immer v1.9+ ondersteunt alleen TypeScript v3.1+.

Opmerking: Flow-ondersteuning kan in toekomstige versies worden verwijderd; we raden TypeScript aan.