Руководство по TypeScript¶
Базовое использование¶
Разница при использовании TypeScript заключается в том, что вместо того, чтобы писать create(...)
, вы должны писать create<T>()(...)
(обратите внимание на дополнительные скобки ()
вместе с параметром type), где T
- это тип состояния, которое нужно аннотировать. Например:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Почему мы не можем просто вывести тип из начального состояния?
TLDR: Поскольку общее состояние T
инвариантно.
Рассмотрим эту минимальную версию create
:
1 2 3 4 5 6 7 8 9 10 11 |
|
Здесь, если вы посмотрите на тип f
в create
, то есть (get: () => T) => T
, он "дает" T
через return (делая его ковариантным), но он также "берет" T
через get
(делая его контравариантным). "Так откуда же берется T
?" задается вопросом TypeScript. Это похоже на проблему курицы или яйца. В конце концов TypeScript сдается и выводит T
как unknown
.
Таким образом, до тех пор, пока родовое понятие, которое нужно вывести, является инвариантным (то есть как ковариантным, так и контравариантным), TypeScript не сможет его вывести. Другим простым примером может быть следующий:
1 2 |
|
Здесь снова x
- это unknown
, а не string
.
Подробнее о выводе (только для тех, кто интересуется TypeScript)
В каком-то смысле этот сбой в выводе не является проблемой, потому что значение типа <T>(f: (t: T) => T) => T
не может быть записано. То есть вы не можете написать реальную реализацию createFoo
. Давайте попробуем это сделать:
1 |
|
createFoo
должен вернуть возвращаемое значение f
. Для этого сначала нужно вызвать f
. А для вызова нам нужно передать значение типа T
. А чтобы передать значение типа T
, его нужно сначала произвести. Но как мы можем получить значение типа T
, если мы даже не знаем, что такое T
? Единственный способ произвести значение типа T
- это вызвать f
, но тогда для вызова самого f
нам нужно значение типа T
. Таким образом, вы видите, что на самом деле невозможно написать createFoo
.
Итак, мы говорим, что сбой вывода в случае createFoo
на самом деле не является проблемой, потому что реализовать createFoo
невозможно. Но как насчет сбоя в выводах в случае create
? Это тоже не проблема, потому что реализовать create
тоже невозможно. Минуточку, если невозможно реализовать create
, то как же тогда Zustand реализует его? Ответ заключается в том, что никак.
Zustand лжет, что он реализовал тип create
, но он реализовал только большую его часть. Вот простое доказательство, демонстрирующее несостоятельность. Рассмотрим следующий код:
1 2 3 4 5 6 7 |
|
Этот код компилируется. Но если мы запустим его, то получим исключение: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". Это происходит потому, что get
вернет undefined
до того, как будет создано начальное состояние (следовательно, вы не должны вызывать get
при создании начального состояния). Типы обещают, что get
никогда не будет возвращать undefined
, но изначально он возвращает, а значит, Zustand не смог реализовать его.
И, конечно, Zustand потерпел неудачу, потому что невозможно реализовать create
так, как обещают типы (точно так же, как невозможно реализовать createFoo
). Другими словами, у нас нет типа для выражения фактического create
, который мы реализовали. Мы не можем определить тип get
как () => T | undefined
, потому что это вызовет неудобства, и это все равно будет неправильно, так как get
действительно () => T
в конечном счете, просто при синхронном вызове это будет () => undefined
. Нам нужна какая-то функция TypeScript, которая позволит нам набирать get
как (() => T) & WhenSync<() => undefined>
, что, конечно, крайне надуманно.
Таким образом, у нас есть две проблемы: отсутствие умозаключений и несостоятельность. Отсутствие выводов можно решить, если TypeScript улучшит свои выводы для инвариантов. А несостоятельность можно решить, если TypeScript введет что-то вроде WhenSync
. Чтобы обойти недостаток инференции, мы вручную аннотируем тип состояния. И мы не можем обойти проблему несостоятельности, но это не так важно, потому что это не так уж и много, вызов get
синхронно в любом случае не имеет смысла.
К чему эти кривляния ()(...)
?
TLDR: Это обходной путь для microsoft/TypeScript#10571.
Представьте, что у вас есть такой сценарий:
1 2 3 4 5 6 7 8 9 10 11 |
|
Здесь T
предположительно является string
, а E
предположительно является unknown
. Возможно, вы захотите аннотировать E
как Foo
, потому что вы уверены в том, какую форму ошибки выкинет doSomething()
. Однако вы не можете этого сделать. Вы можете передать либо все дженерики, либо ни одного. Наряду с аннотацией E
как Foo
, вам также придется аннотировать T
как string
, хотя он все равно будет инферирован. Решение состоит в том, чтобы сделать curried-версию withError
, которая ничего не делает во время выполнения. Ее цель - просто позволить вам аннотировать E
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Таким образом, T
будет инферироваться, а вы сможете аннотировать E
. Zustand имеет тот же случай использования, когда мы хотим аннотировать состояние (первый параметр типа), но позволяем другим параметрам быть инферированными.
В качестве альтернативы можно также использовать combine
, который выдает состояние так, что вам не нужно его вводить.
1 2 3 4 5 6 7 8 9 |
|
Будьте осторожны
Мы достигаем этого вывода, немного солгав в типах set
, get
и store
, которые вы получаете в качестве параметров. Ложь заключается в том, что они типизированы так, как будто состояние - это первый параметр, когда на самом деле состояние - это неглубокое слияние ({ ...a, ...b }
) как первого параметра, так и возврата второго параметра. Например, get
от второго параметра имеет тип () => { bears: number }
, что является ложью, так как должно быть () => { bears: number, increase: (by: number) => void }
. А useBearStore
по-прежнему имеет правильный тип; например, useBearStore.getState
имеет тип () => { bears: number, increase: (by: number) => void }
.
На самом деле это не ложь, потому что { bears: number }
все равно является подтипом { bears: number, increase: (by: number) => void }
. Поэтому в большинстве случаев проблем не возникнет. Просто следует быть осторожным при использовании replace. Например, set({ bears: 0 }, true)
скомпилируется, но будет некорректным, так как удалит функцию increase
. Еще один случай, когда следует быть осторожным, - это использование Object.keys
. Object.keys(get())
вернет ["bears", "increase"]
, а не ["bears"]
. Тип возврата get
может привести к подобным ошибкам.
combine
обменивает немного безопасности типов на удобство, заключающееся в отсутствии необходимости писать тип для состояния. Следовательно, вы должны использовать combine
соответствующим образом. В большинстве случаев она работает нормально, и вы можете использовать ее с удобством.
Обратите внимание, что мы не используем curried-версию при использовании combine
, потому что combine
"создает" состояние. При использовании промежуточного ПО, которое создает состояние, нет необходимости использовать curried-версию, потому что состояние теперь можно вывести. Другим промежуточным ПО, создающим состояние, является redux
. Поэтому при использовании combine
, redux
или любого другого пользовательского промежуточного ПО, создающего состояние, мы не рекомендуем использовать curried-версию.
Использование промежуточного ПО¶
Чтобы использовать промежуточные модули в TypeScript, не нужно делать ничего особенного.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Просто убедитесь, что вы используете их непосредственно внутри create
, чтобы контекстное заключение работало. Для того чтобы сделать что-то даже отдаленно похожее на следующие myMiddlewares
, потребуются более продвинутые типы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Кроме того, мы рекомендуем использовать промежуточное ПО devtools
как можно реже. Например, когда вы используете его с immer
в качестве промежуточного ПО, это должно быть devtools(immer(...))
, а не immer(devtools(...))
. Это связано с тем, что devtools
мутирует setState
и добавляет к нему параметр типа, который может быть потерян, если другие промежуточные программы (например, immer
) также мутируют setState
перед devtools
. Поэтому использование devtools
в конце гарантирует, что никакие промежуточные программы не будут мутировать setState
до него.
Авторские промежуточные модули и расширенное использование¶
Представьте, что вам нужно написать следующее гипотетическое промежуточное ПО.
1 2 3 4 5 6 7 8 9 10 11 |
|
Посреднические программы Zustand могут мутировать хранилище. Но как мы можем закодировать мутацию на уровне типов? То есть как мы можем набрать foo
так, чтобы этот код компилировался?
Для обычного статически типизированного языка это невозможно. Но благодаря TypeScript в Zustand появилось так называемый "мутатор высшего рода", который делает это возможным. Если вы имеете дело со сложными проблемами типов, такими как типизация промежуточного программного обеспечения или использование типа StateCreator
, вам придется разобраться в этой детали реализации. Для этого вы можете посмотреть #710.
Если вам не терпится узнать, каков ответ на эту конкретную проблему, то вы можете посмотреть здесь.
Общие рецепты¶
Middleware, которое не меняет тип хранилища¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
Middleware, изменяющее тип хранилища¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
create
без использования curried обходного пути¶
Рекомендуемый способ использования create
- это использование curried обходного пути, как показано ниже: create<T>()(...)
. Это связано с тем, что так вы сможете определить тип хранилища. Но если по какой-то причине вы не хотите использовать это обходное решение, вы можете передать параметры типа следующим образом. Обратите внимание, что в некоторых случаях это работает как утверждение, а не аннотация, поэтому мы не рекомендуем это делать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Slices паттерн¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
Подробное объяснение шаблона slices можно найти здесь.
Если у вас есть некоторые промежуточные модули, то замените StateCreator<MyState, [], [], MySlice>
на StateCreator<MyState, Mutators, [], MySlice>
. Например, если вы используете devtools
, то это будет StateCreator<MyState, [["zustand/devtools", never]], [], MySlice>
. Список всех мутаторов смотрите в разделе "Middlewares и ссылки на их мутаторы".
Ограниченный хук useStore
для ванильных хранилищ¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Вы также можете сделать абстрактную функцию createBoundedUseStore
, если вам нужно часто создавать ограниченные хуки useStore
, и вы хотите, чтобы все было более рационально...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Middlewares и ссылки на их мутаторы¶
devtools
—["zustand/devtools", never]
persist
—["zustand/persist", YourPersistedState]
YourPersistedState
is тип состояния, которое вы собираетесь сохранять, т.е. возвращаемый типoptions.partialize
, если вы не передаетеpartialize
опции, тоYourPersistedState
становитсяPartial<YourState>
. Также иногда передача фактическогоPersistedState
не работает. В таких случаях попробуйте передатьunknown
.immer
—["zustand/immer", never]
subscribeWithSelector
—["zustand/subscribeWithSelector", never]
redux
—["zustand/redux", YourAction]
combine
- нет мутатора, так какcombine
не мутирует хранилище
Источник — https://docs.pmnd.rs/zustand/guides/typescript