Previously, I was working on a project and decided to make it a personal challenge for myself by using TypeScript for it. I came to understand the beauty of it despite the bouts of confusion and cluelessness. In here, I document my learnings with reference to my project.
Learnings
1. Generic Types
In my application, I worked with grids on two separate occasions — display of categories and services. To ensure reusability of components, I had to create a grid component which accepts both category and service types. Depending on the type received, it will render a different output.
First, I created my CardGrid
component which accepts type
and cards
as parameters.
const CardGrid = <T extends CardTypeProps>({
type,
cards,
}: CardGridProps<T>): ReactNode => {
...
}
Next, I specified the type of the card grid component through the syntax <T extends CardTypeProps>
where CardTypeProps
either holds generic
(represents category) or detailed
(represents service). In doing so, the card grid component type is constrained to the types listed in CardTypeProps
.
type CardTypeProps = 'generic' | 'detailed';
Moving on to the parameters, by denoting CardGridProps<T>
, it specifics the expected structure of props that need to be provided to the CardGrid
component. Additionally, generic constraints is applied here as well where the type of the cards
parameter depends on the type <T>
passed in.
type CardGridProps<T extends CardTypeProps> = {
type: T;
cards: CardProps<T>;
};
Conditional types are used to determine the type of the cards
parameter. If <T>
is generic
, CardProps
resolves to CardGenericObjectProps
; otherwise, it resolves to CardServicesProps
.
type CardProps<T extends CardTypeProps> = T extends 'generic'
? CardGenericObjectProps
: CardServicesProps;
Lastly, the use of ReactNode
signifies that the CardGrid
component returns a ReactNode
, which is the type typically returned by React components.
2. Type Annotations vs Type Assertions
After having lost count of the number of times I got hit with this error, I eventually got the hang of it after understanding the difference between type annotations and type assertions.
‘xxx’ is of type ‘unknown’
Type Annotations explicitly specifies the types of variables, parameters and return values using the syntax : Type
. When a type which is incompatible is assigned, an error will be thrown.
In the following example, service: CardDetailedFormInputProps
explicitly tells TypeScript the expected type of the service
object, ensuring that it is recognized with the specified props.
const remainingServices = categoryServices.filter(
(service: CardDetailedFormInputProps) => service.id !== id
);
Type Assertions informs the TypeScript compiler about the type of a value, overwriting the default inference. It uses the syntax value as Type
or <Type>
.
In the example below, category as CardGenericProps
casts the category
object to the type CardGenericProps
. This is used when TypeScript is unable to infer the type of the variable and I hold more information about the specific type expected.
const remainingCategories = categories.filter(
(category) => (category as CardGenericProps).id !== action.payload!.id
);
3. Index Signatures
Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type
This error happens when TypeScript is unable to determine if the string used to access the object property exists on the object’s type.
For instance, when I tried accessing the PROTECTED_SERVICE_IDS
using category
which is a string, TypeScript is unable to verify that the category
variable passed in exists in PROTECTED_SERVICE_IDS
.
To address this, type assertions can be applied. By specifying the structure of PROTECTED_SERVICE_IDS
, the type of the object can be retrieved using keyof typeof
.
typeof
retrieves the type of categoryProps
as {body: [], face: [], ...}
. keyof
then extracts the keys inferred from typeof
, returning a union of the keys as such 'body' | 'face' | ...
.
This ensures that category
will always be treated as one of the keys in categoryProps
.
const categoryProps = {
body: [],
face: [],
hair: [],
'hair removal': [],
nail: [],
};
type CategoryKey = keyof typeof categoryProps;
const selectedCategory: string[] =
PROTECTED_SERVICE_IDS[category as CategoryKey];