Keep Store type sync with definition
Avinash
Posted on February 17, 2024
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
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
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.
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.
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,
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
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.
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.
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.
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
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.
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
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
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.
**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.
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
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
type expandTypes<T> = T extends infer O ? {
[P in keyof O]: expandTypes<O[P]>
} : never
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.
Posted on February 17, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024