redux-orm icon indicating copy to clipboard operation
redux-orm copied to clipboard

Typescript usage guidance

Open yang opened this issue 5 years ago • 14 comments

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!

yang avatar Mar 07 '19 06:03 yang

Would love answers to this one as well :D

kg-currenxie avatar Jun 30 '19 15:06 kg-currenxie

@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.

tomasz-zablocki avatar Jul 01 '19 07:07 tomasz-zablocki

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:

  1. Property 'name' does not exist on type 'Book'
  2. Property 'id' does not exist on type 'ModelType<Book>'

PS: What is modelName and toString actually used for?

Thanks, love redux-orm so far ❤️❤️

kg-currenxie avatar Jul 01 '19 10:07 kg-currenxie

  1. 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}`;
    }
}
  1. 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.

  1. 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.

  1. toString is an optional override for Object.prototype.toString allowing for custom textual representations. You don't have to provide toString implementations unless you want custom text out of console.log etc.

I'll check the selector issue once my back home today.

tomasz-zablocki avatar Jul 01 '19 11:07 tomasz-zablocki

Thanks for the elaborate responses :)

  1. 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 avatar Jul 01 '19 11:07 kg-currenxie

@kg-currenxie I've created a PR fixing createSelector typings, test file has new cases to refer for examples

tomasz-zablocki avatar Jul 09 '19 01:07 tomasz-zablocki

Will test this weekend!

kg-currenxie avatar Jul 11 '19 08:07 kg-currenxie

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 avatar Jul 16 '19 12:07 kg-currenxie

@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.

tomasz-zablocki avatar Jul 16 '19 14:07 tomasz-zablocki

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

tiangolo avatar Nov 21 '19 07:11 tiangolo

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 avatar May 02 '22 20:05 dpaladin

@dpaladin , I fixed a similar error by downgrading my version of Typescript to 3.8

Darasimi-Ajewole avatar Jul 13 '22 01:07 Darasimi-Ajewole

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

troberts-28 avatar Jul 13 '22 16:07 troberts-28