Yannick Serra
Yannick Serra

Reputation: 370

Problem to type a generic typescript structure

I would like to deal with data like

const stateTest  = {
  natures : {
    nature1: {
      amounts: {
        column1: 10,
        column2: 10,
      },
      natureDetails : {
        detail1 : {
          amounts: {
            column1: 10,
            column2: 10,
          },
          descriptionShown: false
        }
      }
    }
  }
}

where the list of columns (here column1,column2) is a parameter

so I declared the following types and interfaces

type State<T extends GenericColumnList> = T & {
  natures: naturesSet<T>
}
type GenericColumnList = Record<string, number>
type naturesSet<T extends GenericColumnList> = Partial<Record<string, RowNature<T>>>
export interface RowNature<T extends GenericColumnList> {
  natureDetails: NatureDetailsSet<T>
  amounts: T
}
type NatureDetailsSet<T> = Partial<Record<string, RowDetailNature<T>>>
export interface RowDetailNature<T> {
  amounts: T
  descriptionShown: boolean
}

but when I try to pass the following columnList interface as parameter to State

export interface MyColumns  {
  column1?: number
  column2?: number
}

that is :

const stateTest: State<MyColumns>  = {
  natures : {
    nature1: {
      amounts: {
        column1: 10,
        column2: 10,
      },
      natureDetails : {
        detail1 : {
          montants: {
            column1: 10,
            column2: 10,
          },
          descriptionShown: false
        }
      }
    }
  }
}

The typescript compiler complains that

'The type MyColumns doesn't comply with Record<string, number> ' and I do not understand why.

Upvotes: 0

Views: 51

Answers (1)

jcalz
jcalz

Reputation: 329248

The type Record<string, number> has a string index signature. The interface MyColumns does not have an index signature, and so the types are not compatible.


There is such a concept as an implicit index signature, in which types without an explicit index signature are seen as compatible with an indexable type as long as all the known properties conform to the index signature... but this does not apply to types declared as an interface; it only works with anonymous types (or type aliases of such anonymous types). There is an open issue in GitHub, microsoft/TypeScript#15300, discussing this. It turns out that, for now anyway, this behavior is by design.

So one way to deal with this is to make MyColumns a type alias of an anonymous type instead of any interface, like this:

export type MyColumns = {
  column1?: number
  column2?: number
}

Then, you will need to include undefined in the domain of GenericColumnList because the type of MyColumns['column1'] is number | undefined and not just number (assuming we are using the recommended --strict compiler options including --strictNullChecks):

type GenericColumnList = Record<string, number | undefined>

And then your assignment works:

const stateTest: State<MyColumns> = {
  natures: {
    nature1: {
      amounts: {
        column1: 10,
        column2: 10,
      },
      natureDetails: {
        detail1: {
          amounts: {
            column1: 10,
            column2: 10,
          },
          descriptionShown: false
        }
      }
    }
  }
}; // okay

Assuming we don't want to require people use non-interface types for T, we could instead make GenericColumnList itself generic in T so that instead of an index signature it just has the same keys as T:

type GenericColumnList<T> = { [K in keyof T]?: number } 

Here we've made the properties optional (?) for the same reason as adding | undefined earlier: to make optional/missing keys compatible in the presence of --strictNullChecks.

This requires some sprinkling of <T> around your code:

type State<T extends GenericColumnList<T>> = T & {
  natures: NaturesSet<T>
}
type NaturesSet<T extends GenericColumnList<T>> = Partial<Record<string, RowNature<T>>>
export interface RowNature<T extends GenericColumnList<T>> {
  natureDetails: NatureDetailsSet<T>
  amounts: T
}

and again, your assignment will work:

const stateTest: State<MyColumns> = {
  natures: {
    nature1: {
      amounts: {
        column1: 10,
        column2: 10,
      },
      natureDetails: {
        detail1: {
          amounts: {
            column1: 10,
            column2: 10,
          },
          descriptionShown: false
        }
      }
    }
  }
}; // okay

Playground link to code

Upvotes: 1

Related Questions