Reputation: 370
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
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
Upvotes: 1