Keep Store type sync with definition
Tags: typescript, javascript
Published: 2024-02-17 05:26:19

---
title: Keep Store type sync with definition
published: true
description: Typescript has some utility function that makes our life easier. Readonly is one of the utilities provided by Typescript. We will look into Readonly utility and how to create a custom typescript utility on top of it.
tags: #typescript #javascript
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-02-15 05:30 +0000
---

##Introduction
I wanted to drive the types from the store definition itself. To achieve this, I learned about TypeScript utilities and how to create custom ones directly from the store definition itself.

Before diving directly into the final utility, we go through some of the functionality that is provided by the typescript that is used in the final utility created.
 
To view the code jump to the [code section](#utility-code)

### Const assertions
Const assertion tells typescript that to type will not change its length and content. So what it means is suppose the type is

```tsx
const arr1 = ['a','b'];

//type is 
// string[]

const arr = ['a','b'] as const;

// type is
// readonly ["a", "b"]
```

### Conditional Types

The syntax of the conditional types is similar to the ternary operator in JavaScript. Typescript uses extends to check the type and based on the check it either returns a truthy type or a falsy type. 

```tsx
type A<Type> = Type extends string ? Type: never;

const a: A<'A simple imutable string'>  = 'A simple imutable string'; // type of a: 'A simple imutable string'

const str: A<1> = 1 // error: Type 'number' is not assignable to type 'never'
```

**Infer keyword**

In typescript, we can use Infer to gather the type of type using the infer keyword. 

```tsx
type A<Type> = Type extends Array<infer Item> ? Item : never;

const A = ['s','a'];
const D = [1,2,3,4,5];

type B = A<typeof A> // string
type C = A<typeof D> // number
```

This is useful when it comes to inferring the type of items present in the array. One thing to note about infer is that infer should only be used in conditional types. 

Infer can also be used to check the return type of function. For example,

```tsx
type A <Func> = Func extends (...args: any) => infer Return? Return: never

const funcToReturnString = () => 's'

type B = A<typeof funcToReturnString> // type of B is string
```

### Mapped Type

Typescript provides a way to get the type of keys in the object. We can reduce the redundant steps of mapping all the properties in object using **Mapped types**

```tsx
type A<Type> = {
	[property in keyof Type]: boolean
}
```

The above type will map all properties in objects to the boolean type.
In typescript the modifiers ( are responsible for changing the mutability or optionality) of object property using `Readonly` of`?` operator in object property. 

```tsx
type ImmutableObj = {
	readonly name: string;
	readonly uuid: string;
}

type OptionalType = {
	email?: string;
	address?: string;
}
```

We can change the optionality or immutability of the object properties by adding or removing the modifiers. Let us look into the example.

```tsx
type AllOptional<Type> = {
	[property in keyof Type]+?: Type[property]
}

const RequiredObj = {
	name:'Tony',
	phone: 'X-XXX-XXX'
}

type UserObjType = AllOptional<typeof RequiredObj>
```

There is more to mapped types in [typescript documentation](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html).

### Recursive types

Recursion is the most common concept in many programming languages including javascript. At its core recursive function means calling the same function until it reaches the exist state at which point the recursive function exists.

In the context of the typescript, recursion means. Referencing the same type from its definition.

Understanding by looking into an example. If we look into typescript Readonly utility 

```tsx
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
```

 The problem with the above utility is it doen’t provides readonly type for nested type types such as. 

```tsx
const deep = {
	a:{
		b:{
			c:['sdf', {
				d:'str'
			}]
		}
	}
}

type Deep  = Readonly<typeof deep> 

// type Deep = {
//    readonly a: {
//        b: {
//            c: (string | {
//                d: string;
//            })[];
//        };
//    };
//}
```

 

### Mixing it

The goal is to drive the type definition from the initial state definition. Suppose we define the default state object something like this 

```tsx
const defaultState = {
  isConnected: false,
  currRoom: {
    roomName: "",
    currUser: {},
    allUsers: [
      {
        name: "",
        userId: "",
      },
    ],
  },
  gameState: {
    owner: {},
    remainingLetters: [""],
    isChoosing: false,
    guessWord: "",
    myTurn: false,
    selectedLetters: [""],
    correctSelectedLetters: [""],
    wrongSelectedLetters: [""],
    turn: {
      name: "",
    },
    isCorrect: false,
    incorrect: 0,
    gameOver: false,
  },
}
```

I don’t want to create a type or interface for this object as when I update the default state I’ll have to update the definition as well.  How to achieve DRY for this type of definition.

The above object is the default state from which I want to drive the types. Let us go through all the approaches.

**Using const assertion**

We can simply use const assertion and `typeof` to get the types 

```tsx
const defaultState = {
  isConnected: false,
  currRoom: {
    roomName: "",
    currUser: {},
    allUsers: [
      {
        name: "",
        userId: "",
      },
    ],
  },
  gameState: {
    owner: {},
    remainingLetters: [""],
    isChoosing: false,
    guessWord: "",
    myTurn: false,
    selectedLetters: [""],
    correctSelectedLetters: [""],
    wrongSelectedLetters: [""],
    turn: {
      name: "",
    },
    isCorrect: false,
    incorrect: 0,
    gameOver: false,
  },
} as const

type Store = typeof defaultState 

//type will be 

//type Store = {
//    **readonly isConnected: false;** 
//    readonly currRoom: {
//        readonly roomName: "";
//        readonly currUser: {};
//        readonly allUsers: readonly [{
//            **readonly name: "";**
//            readonly userId: "";
//        }];
//    }; ....
```

we can see that the type of `isConnected` is false but we wanted TS to infer it as `boolean`. Another limitation is `readonly name: ""`  is typed as an empty string rather than type `string`.

**Using Readonly utility** 

We can also use the `Readonly` utility type provided by Typescript. 

```tsx
 **const defaultState = {
  isConnected: false,
  currRoom: {
    roomName: "",
    currUser: {},
    allUsers: [
      {
        name: "",
        userId: "",
      },
    ],
  },
  gameState: {
    owner: {},
    remainingLetters: [""],
    isChoosing: false,
    guessWord: "",
    myTurn: false,
    selectedLetters: [""],
    correctSelectedLetters: [""],
    wrongSelectedLetters: [""],
    turn: {
      name: "",
    },
    isCorrect: false,
    incorrect: 0,
    gameOver: false,
  },
};

type Store = Readonly<typeof defaultState>

//Store Type
// type Store = {
//     readonly isConnected: boolean;
//     readonly currRoom: {
//         roomName: string;
//         currUser: {};
//         allUsers: {
//             name: string;
//             userId: string;
//         }[];
//     };
//     readonly gameState: {
//         owner: {};
//         remainingLetters: string[];
//         isChoosing: boolean;
//         ... 8 more ...;
//         gameOver: boolean;
//     };
// }**
```

The `Readonly` utility type correctly infers type but it doesn’t add readonly to nested properties of an object. If we look into the implementation of the `Readonly` utility type.

```tsx
type Readonly<T> = {
	readonly [P in keyof T]: T[P];
}
```

Readonly utility adds `readonly` to all the top-level properties of the object but doesn't iterate over the nested object. 

**Creating custom utility**

We can extend your knowledge of `Readonly` utility by using recursive types

```tsx
type ReadonlyNested<S> = { readonly [K in keyof S]: ReadonlyNested<S[K]> };

// type Store = {
//     readonly isConnected: ReadonlyNested<boolean>;
//     readonly currRoom: ReadonlyNested<{
//         roomName: string;
//         currUser: {};
//         allUsers: {
//             name: string;
//             userId: string;
//         }[];
//     }>;
//     readonly gameState: ReadonlyNested<...>;
// }
```

Let us go a step further and create another type of utility called expand

```tsx
type expandTypes<T> = T extends infer O ? {
  [P in keyof O]: expandTypes<O[P]>
} : never
```
<a id="utility-code"></a>
Now when we do expansion

```tsx
const defaultState = {
  isConnected: false,
  currRoom: {
    roomName: "",
    currUser: {},
    allUsers: [
      {
        name: "",
        userId: "",
      },
    ],
  },
  gameState: {
    owner: {},
    remainingLetters: [""],
    isChoosing: false,
    guessWord: "",
    myTurn: false,
    selectedLetters: [""],
    correctSelectedLetters: [""],
    wrongSelectedLetters: [""],
    turn: {
      name: "",
    },
    isCorrect: false,
    incorrect: 0,
    gameOver: false,
  },
};

type ReadonlyNested<T> = {
	readonly [P in keyof T]: ReadonlyNested<T[P]>;
}

type expandTypes<T> = T extends infer O ? {
  [P in keyof O]: expandTypes<O[P]>
} : never

type Store = expandTypes<ReadonlyNested<typeof defaultState>>

// Store type

// type Store = {
//     readonly isConnected: ReadonlyNested<boolean>;
//     readonly currRoom: {
//         readonly roomName: string;
//         readonly currUser: {};
//         readonly allUsers: readonly {
//             readonly name: string;
//             readonly userId: string;
//         }[];
//     };
//     readonly gameState: {
//         ...;
//     };
// }
```

### Conclusion

After syncing the type definition with the store object makes a great developer experience. We can have more custom utility for typescript. Please add a custom utility that you find interesting in the commotion section.