DEV Community

Cover image for Modeling the state is your responsibility
Pragmatic Maciej
Pragmatic Maciej

Posted on • Updated on

Modeling the state is your responsibility

State - the thing which is always with us devs, no matter what paradigm do you use, state is there. It can be in redux, mobx, it can be global state, local state, function state, module state. State is everywhere. Lets talk about modeling the state.

Modeling the point

The inspiration for this article comes from this StackOverflow question stackoverflow question, where the asking person wants to create a structure which is type safe and have members in a form {name}px like for example 1px or 10px, but other formats should not be allowed.

Lets dive into the question and what problem the questioner has. Directly the question is - How to create a type which value has a form:

const position = {
  x: '1px',
  y: '2px'
}
Enter fullscreen mode Exit fullscreen mode

and be still type safe. The first impression and the real answer to this question is - its not possible at least in most of languages and for sure not possible in TypeScript. But... let's consider what really we want to achieve. The origin of the problem is - how to model a state which has x,y values and unit information, and be type safe. And if we ask this question in that way, there is not even one answer, but many, we need to pick the most efficient one, and this which match the need.

What type can have these three informations included. Yes! The tuple (in TS it is just array with strict length) - type Position = [number, number, string]. Let's see how to use this:

const position: Position = [1,2,'px'];
const createPxPosition = (x,y): Position => [x, y, 'px']; // value constructor
const positionCreatedByConstructor = createPxPosition(1,2) // [1,2,'px']
Enter fullscreen mode Exit fullscreen mode

Nice, but it is not single options which have a sense here. Let's consider next type which we can use - type Position = [[number, string],[number, string]].

const createPxPosition = (x,y): Position => [[x, 'px'], [y, 'px']]; // value constructor
const position: Position = createPxPosition(1,2) // [[1,'px'],[2,'px']]
Enter fullscreen mode Exit fullscreen mode

Both versions are totally type safe and valid. As Tuple is product type which is isomorhic to another product type - Record (in TS key-value map). So the same approach with Records:

type Position1 = {x: number, y: number, unit: string};
const position: Position1 = {x: 1, y: 2, unit: 'px'};
// mix of tuple and record:
type Position2 = {x: [number, string], y: [number, string]};
const position2: Position2 = {x: [1, 'px'], y: [2, 'px']};
Enter fullscreen mode Exit fullscreen mode

Let's go further - why we allow on string type if unit is static. With TS it is not a problem, we can define const type with one value - px.

type Position = {x: number, y: number, unit: 'px'};
const position: Position = {x: 1, y: 2, unit: 'px'}; // only px value possible
Enter fullscreen mode Exit fullscreen mode

The last but not least in this example is - why at all we need px in the Point type if this is static value? And this question is fully correct, as probably we need just a pair - [number, number] and static unit constant. But still what I wanted to show here is - state is not something which is given, and cannot be changed. Here developer needs to model three informations - x,y as numbers and the unit as string or const type. It can be done in many ways, we should not get into some dead ends like trying to type safe string concatenation {number}px.

Arrays with the same length in type?

To prove that we can shape the state to match our needs, I will present another case - I want to model function output type to ensure that function will return two arrays with the same length at the compilation time. Impossible you say? Looks like, but let's use our imagination and try something:

// naive try - record with two arrays (string and number is randomly picked type element)
type MyReturn1 = {arr2: string[], arr2: number[]}
Enter fullscreen mode Exit fullscreen mode

What this type quarantee - that we need to return two arrays, yep, but does it require the same length of them? Nope it does not. So the type is not enough.

// better try - array of tuples
type MyReturn2 = [string, number][]
Enter fullscreen mode Exit fullscreen mode

Supprised? Let me explain now, in fact it does not match our original question, as I do not return two arrays. But again the question is, if I havent specified too much technical detail in the question , and not considered the higher goal. As fact the higher goal is to have the same amount of values of type string and of type number. And this exactly MyReturn2 achieves. Type ensures that for three strings we will have three numbers. Wow that works!

Modelling the type is a sport itself. We can really achieve incredible results.

Map or Record structure?

What if we have an array of elements, and the standard operation is to take from this array element by its id?

type Person = {id: number, name: string} // element type
type Persons = Person[]

Enter fullscreen mode Exit fullscreen mode

Ok looks good, but how to then take element by id. This operation needs to traverse the array:

function findPersonById(id: number, persons: Persons) {
    return persons.find(person => person.id === id)
}
Enter fullscreen mode Exit fullscreen mode

Nothing specially wrong with that, but we can do better and model the shape of the state to match the need.

type PersonId = number // type alias for readability
type Person = {id: PersonId, name: string} // element type
type Persons = Record<PersonId, Person>

function findPersonById(id: number, persons: Persons) {
    return persons[id] // done yes :D
}

Enter fullscreen mode Exit fullscreen mode

What we gain is constant access time of getting the Person by id and less complexity of this operation. Of course the last case has a tradeoff, so it is possible that modeling as array will fit the need better, but still we have a choice.

To sum it up. What I wanted to tell here is - do not skip state modeling, do not take whatever the server sends, even if response does not match your needs, tell about that loudly, model the state on FE side, or ask about this modeling on the BE side. We are responsible for state modeling, we should spend on this process more time, as it will save the time later in accessing and transformation operations.

Top comments (0)