redux-orm
redux-orm copied to clipboard
Typescript usage guidance
I know there are multiple issues about Typescript support, but these mostly just point out the type definitions for the library itself.
My question is: how is one supposed to use redux-orm from Typescript? For instance, off the top of my head:
- How does one declare models with Typescript types and get strong types throughout the interface?
- How does one declare the types on foreign keys?
- How does one declare recursive types?
- How does one refer to the types representing the raw values vs. types of the redux-orm wrappers?
Thanks!
Would love answers to this one as well :D
@kg-currenxie See typings tests source for an example and let me know if it helped or in case you have any specific questions regarding redux-orm typing.
I'd love to create detailed guidelines and I certainly will - once I manage to resolve typing issues related to type depth limitations, which are a blocker for some people out there.
Thanks for the link! I've solved many of my any
types ;)
Found one issue:
createSelector
is defined as:
export function createSelector<I extends IndexedModelClasses<any>, Result extends any = any>(
orm: ORM<I>,
ormSelector: ORMSelector<I, Result>
): (state: OrmState<I>) => Result;
Selector code
const authorSelector = createSelector(
orm,
dbStateSelector,
(session: any) => {
return session.Book.all().toRefArray()
},
)
Saying Expected 2 arguments, but got 3.
Help
I couldn't figure out some other cases: Could you help me with these? 🙏
Book model:
import { attr, Model, ModelType } from 'redux-orm'
import { RootAction } from '@actions'
import { CREATE_BOOK } from '@actions/bookActions'
interface BookFields {
id: number
name: string
}
export class Book extends Model<typeof Book, BookFields> {
static modelName = 'Book'
static fields = {
id: attr(),
name: attr(),
}
toString() {
return `Book: ${this.name}` //<------------------- Warning 1. see below
}
static reducer(action: RootAction, BookModel: ModelType<Book>) {
...
}
}
export type BookType = ModelType<Book>
// export type BookType = Book.fields <- Actually removes "Warning 2.", but seems wrong.
App.tsx
export class App extends Component<AppProps> {
createBook = () => {
this.props.createBook({ name: 'Test' })
}
renderBook = (book: BookType) => {
return (
<View key={`Book${book.id}`}> //<---------------- Warning 2. see below
<Text>{book.name}</Text>
</View>
)
}
renderBooks = () => {
return this.props.books.map(this.renderBook)
}
render() {
return (
<View style={styles.main}>
<Button onPress={this.createBook} style={styles.button}>
<Text style={styles.buttonText}>Create book!</Text>
</Button>
{this.renderBooks()}
</View>
)
}
}
Warnings:
-
Property 'name' does not exist on type 'Book'
-
Property 'id' does not exist on type 'ModelType<Book>'
PS: What is modelName
and toString
actually used for?
Thanks, love redux-orm so far ❤️❤️
- Warning 1:
Since the properties are not present on model classes and actually they are dynamically defined in runtime, getting static type assistance for them is achieved via second type parameter (in your case that's BookFields
). To remove Property 'name' does not exist on type 'Book'
you can hint the this
type to class method like this:
export class Book extends Model<typeof Book, BookFields> {
static modelName = 'Book';
static fields = {
id: attr(),
name: attr()
};
//this: BookFields would also do the trick, but SessionBoundModel<Book> is what **this** actually points to
toString(this: SessionBoundModel<Book>) {
return `Book: ${this.name}`;
}
}
- Warning 2:
Yes that,'s wrong. ModelType
is the class/constructor function of the model, with type parameters narrowed on methods like create/update/upsert/withId
and queries like all/first/last
etc. What you wanted to use is SessionBoundModel<Book>
. However, by passing Model
instances to components, you're pretty much exposing bare redux store to you react components, which is against core redux principles. What you should do is query you redux store using orm selectors and pass the data in mapStateToProps
. There's a redux-orm issue which covers this topic in detail, I'll try to look it up once I'm done with my daily duties.
- Mandatory
modelName
(quoting README):
When declaring model classes, always remember to set the modelName property. It needs to be set explicitly, because running your code through a mangler would otherwise break functionalitymodelName will be used to resolve all related fields.
-
toString
is an optional override forObject.prototype.toString
allowing for custom textual representations. You don't have to providetoString
implementations unless you want custom text out ofconsole.log
etc.
I'll check the selector issue once my back home today.
Thanks for the elaborate responses :)
- I'm actually using
mapStateToProps
, sorry I didn't paste all of it. Here:
import React from 'react'
import { connect } from 'react-redux'
import { Button, Component, Styles, Text, View } from 'reactxp'
import { bindActionCreators, Dispatch } from 'redux'
import { getShow } from '@actions/showActions'
import { ShowType } from '@models'
import { showsSelector } from '@selectors/showSelectors'
import { AppState } from '@types'
const styles = {
...
}
type AppProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>
export class App extends Component<AppProps> {
getShow = () => {
this.props.getShow({ permalink: 'game-of-thrones' })
}
renderShow = (show: ShowType) => {
return (
<View key={`Show${show.id}`}>
<Text>{show.name}</Text>
</View>
)
}
renderShows = () => {
return this.props.shows.map(this.renderShow)
}
render() {
return (
<View style={styles.main}>
<Button onPress={this.getShow} style={styles.button}>
<Text style={styles.buttonText}>Get show!</Text>
</Button>
{this.renderShows()}
</View>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
shows: showsSelector(state),
}
}
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
getShow,
},
dispatch,
)
export const ConnectedApp = connect(
mapStateToProps,
mapDispatchToProps,
)(App)
Changing export type BookType = ModelType<Book>
to export type ShowType = SessionBoundModel<Show>
seems to at least remove the warning, even tho it's not the correct way :) Which issue # is it?
@kg-currenxie I've created a PR fixing createSelector typings, test file has new cases to refer for examples
Will test this weekend!
Seems to work fine now! :D
Having other issues, but it has to do with integrating with actions from https://github.com/aikoven/typescript-fsa
I guess not technically a redux-orm issue
@kg-currenxie I haven't tried typescript-fsa myself, but I've integrated redux-orm with typesafe-actions without issues.
I find the latter to be a really robust and well-thought-through solution and had no need to look for a replacement - just a personal opinion.
For anyone using the typing tests as a template/reference, the ones in DefinitelyTyped (the main repo where types live) seems to be a bit more updated: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/redux-orm/redux-orm-tests.ts
I keep getting Type 'typeof Book' does not satisfy the constraint 'typeof AnyModel'. Types of construct signatures are incompatible. Type 'new (props: BookFields) => Book' is not assignable to type 'new (props: any) => AnyModel'. Type 'Book' is not assignable to type 'AnyModel'. Type 'string' is not assignable to type '"Book"'.
when i try something like this class Book extends Model<typeof Book, BookFields>
even using the ones in redux-orm-tests.ts
@dpaladin , I fixed a similar error by downgrading my version of Typescript to 3.8
I keep getting Type 'typeof Book' does not satisfy the constraint 'typeof AnyModel'. Types of construct signatures are incompatible. Type 'new (props: BookFields) => Book' is not assignable to type 'new (props: any) => AnyModel'. Type 'Book' is not assignable to type 'AnyModel'. Type 'string' is not assignable to type '"Book"'.
when i try something like this class Book extends Model<typeof Book, BookFields>
even using the ones in redux-orm-tests.ts
Facing the same issue as this. Downgrading my Typescript version didn't help