TypeScript Tips
Zedux was created in TypeScript from the ground up. Many Zedux APIs are designed specifically to work well with TypeScript. The API Docs document many of Zedux's types, but there are several TypeScript helpers and nuances not covered anywhere else in these docs.
- How to use Zedux's
AtomGenerics
"type map" - Some utility types that Zedux exports
- How to pull generics off Zedux classes
- Some shortcomings and workarounds
AtomGenerics
When creating functions (injectors, hooks, utility functions, etc), you'll often want to accept atom templates or instances matching a certain type. The AtomTemplate and AtomInstance classes require a single AtomGenerics
generic. This generic is actually an object type containing several other generics that are all required for every atom template and instance.
This AtomGenerics
object is called a "type map". It's kind of like passing an object to a function instead of a long list of params. The object lets you name the params and only pass the ones you want. But instead of passing params to a function, it's passing generics to a class.
It looks like this:
interface AtomGenerics {
Exports: Record<string, any>
Params: any[]
Promise: Promise<any> | undefined
State: any
Store: Store<any>
}
Several Zedux APIs accept an AtomGenerics
type map. Atom templates and instances use it, so you'll probably see it a lot. Factories like atom()
automatically infer these types and map them into this type map when you define atoms.
Utility Types
All AtomGenerics
are required. But sometimes it can be a real pain to pass all of them. Sometimes you only care that the state is a string. Sometimes you don't care about any of them. For these situations, Zedux exports two utility types:
AnyAtomTemplate
- Configures all or part of the AtomTemplate class's generics.AnyAtomInstance
- Configures all or part of the AtomInstance class's generics.
AnyAtomTemplate
Use this with no generics to accept absolutely any atom template:
const getKey = <A extends AnyAtomTemplate>(template: A) => template.key
Pass any number of the AtomGenerics
to accept only atom templates that match those generics:
let idCounter = 0
const instantiateWithId = <A extends AnyAtomTemplate<{ Params: [id: string] }>>(
template: A
) =>
ecosystem.getInstance(template, [
(idCounter++).toString(),
] as AtomParamsType<A>) // this cast is necessary. Current TS shortcoming.
The as AtomParamsType<A>
cast here is necessary. We'll look at that helper shortly. But know that with the cast in place, TS will still error if your type is wrong. The cast only prevents it from erroring when your type is right. Thus you're still getting full type safety, just with one extra step.
AnyAtomInstance
Use this with no generics to accept absolutely any atom instance:
const getKey = <I extends AnyAtomInstance>(instance: I) => instance.template.key
Pass any number of the AtomGenerics
to accept only atom instances that match those generics. For example, to accept any atom instance whose state type is a string, use the following:
const getUpperCase = <I extends AnyAtomInstance<{ State: string }>>(
instance: I
) => {
// TS knows this `.getState()` returns a string:
return instance.getState().toUpperCase()
}
Generics Getters
To pull AtomGenerics
off of an atom template or atom instance, use one of these utils:
AtomExportsType
- pull theExports
type off a template or instance.AtomParamsType
- pull theParams
type off a template or instance.AtomPromiseType
- pull thePromise
type off a template or instance.AtomStateType
- pull theState
type off a template or instance.AtomStoreType
- pull theStore
type off a template or instance.
Example:
import {
AtomExportsType,
AtomParamsType,
AtomPromiseType,
AtomStateType,
AtomStoreType,
api,
atom,
injectStore,
} from '@zedux/react'
const exampleAtom = atom('example', (id: string) => {
const store = injectStore(0)
return api(store).setExports({ id }).setPromise(Promise.resolve(true))
})
type TExports = AtomExportsType<typeof exampleAtom> // { id: string }
type TParams = AtomParamsType<typeof exampleAtom> // [id: string]
type TPromise = AtomParamsType<typeof exampleAtom> // Promise<boolean>
type TState = AtomStateType<typeof exampleAtom> // number
type TStore = AtomStoreType<typeof exampleAtom> // Store<number>
Or using the instance:
const instance = myEcosystem.getInstance(exampleAtom, ['an-id'])
type TExports = AtomExportsType<typeof instance> // { id: string }
type TParams = AtomParamsType<typeof instance> // [id: string]
type TPromise = AtomParamsType<typeof instance> // Promise<boolean>
type TState = AtomStateType<typeof instance> // number
type TStore = AtomStoreType<typeof instance> // Store<number>
Additionally, atom templates have a util to pull the instance type they create, and atom instances have a util to pull the template type they were created from:
AtomInstanceType
- pull the AtomInstance type off an atom templateAtomTemplateType
- pull the AtomTemplate type off an atom instance
import { AtomInstanceType, AtomTemplateType } from '@zedux/react'
type TInstance = AtomInstanceType<typeof exampleAtom>
type TTemplate = AtomTemplateType<typeof instance>
Accepting Atoms
When creating functions, you'll often want to accept a Zedux class that meets some criteria. Here are some examples of how to do this (note that there may be more ways to do these!).
Instance of Template
To create a function that accepts any instance of a given template, use the following:
const getRounded = <I extends AtomInstanceType<typeof exampleAtom>>(
instance: I
) => {
// given the exampleAtom above, TS knows this `.getState()` returns a number:
return Math.round(instance.getState())
}
Matching Exports
The AnyAtomInstance
type can be used to match an atom instance with an exact exports shape:
const validateInstance = <
I extends AnyAtomInstance<{ Exports: { validate: () => boolean } }>
>(
instance: I
) => instance.exports.validate()
However this will only accept atom instances with a validate
function as their only export. To accept any atom instance that contains at least specific exports, use the following:
const validateInstance = <
I extends Omit<AnyAtomInstance, 'exports'> & {
exports: { validate: () => boolean }
}
>(
instance: I
) => instance.exports.validate()
Just a good old TS Omit
and you're in business.
AtomTuple
Most of Zedux's functions require params to be passed when the atom takes params. But if the atom doesn't take params, Zedux makes the params array optional.
Zedux uses overloads to accomplish this. For full power, you may want to use overloads too:
const getValue: {
// params ("family"):
<A extends AnyAtomTemplate>(
template: A,
params: AtomParamsType<A>
): AtomStateType<A>
// no params ("singleton"):
<A extends AnyAtomTemplate>(template: ParamlessTemplate<A>): AtomStateType<A>
// also accept instances 'cause we can 'cause overloads are powerful:
<I extends AnyAtomInstance>(instance: I): AtomStateType<I>
} = <A extends AnyAtomTemplate | AnyAtomInstance>(
template: A,
params?: AtomParamsType<A>
) => {
ecosystem.get(template as AnyAtomTemplate, params) // this cast is fine
}
This gives the most control, but can obviously be tedious to create and maintain. TS is a little limited here, but Zedux does export one util that can help with this - AtomTuple
.
AtomTuple
is a tuple type that contains an atom template and its params type. Use it like so:
const getPromise = <A extends AnyAtomTemplate<{ Promise: Promise<any> }>>(
...[template, params]: AtomTuple<A>
) => ecosystem.getInstance(template, params).promise as AtomPromiseType<A>
getPromise(myParamlessAtom) // works
getPromise(twoParamsAtom, ['param-1', 'param-2']) // works
Now no overloads are needed, at the cost of a little tuple spread ugliness. Of course this doesn't support passing an atom instance like the overloads example. But for functions that don't need that, this may be good enough.
Stores
Zedux also exports a few helper types for working with stores, actions, and action factories:
StoreStateType
- pulls the state generic off a Zedux storeActionFactoryTypeType
- yes - pulls the string action type off an action factoryActionFactoryPayloadType
- pulls the payload type off an action factoryActionFactoryActionType
- pulls the full action type (an object with type and payload properties) off an action factoryActionTypeType
- pulls the string action type off an actionActionPayloadType
- pulls the payload type off an actionActionMetaType
- pulls the meta type off an action
import {
ActionFactoryTypeType,
ActionFactoryPayloadType,
ActionFactoryActionType,
ActionTypeType,
ActionPayloadType,
ActionMetaType,
actionFactory,
createStore,
StoreStateType,
} from '@zedux/react'
type Todo = { text: string }
const store = createStore(null, 'Hello, world!')
type State = StoreStateType<typeof store> // string
const addTodo = actionFactory<Todo>('addTodo')
const action = addTodo({ text: 'Use TS' })
type Test = [
ActionFactoryTypeType<typeof addTodo>, // 'addTodo'
ActionFactoryPayloadType<typeof addTodo>, // Todo
ActionFactoryActionType<typeof addTodo>, // { type: 'addTodo'; payload: Todo }
ActionTypeType<typeof action>, // 'addTodo'
ActionPayloadType<typeof action>, // Todo
ActionMetaType<typeof action> // any
]
Recap
AnyAtomTemplate
helps selectively match atom templates.AnyAtomInstance
helps selectively match atom instances.- The
Atom_____Type
helpers pull generics off atom templates and instances. - The
AtomTuple
type can mitigate the need for function overloads.