Types
One of the main features of TypeScript is its ability to type-check your code. This means that you can specify the types of variables, function parameters, and return values, and the TypeScript compiler will check that your code adheres to these types. For example, you can specify that a function expects a string as an argument and returns a number like this:
function powerOfN(x: number,n: number): number {
return Math.pow(x,n);
}
In this example, if you try to pass a non-number value to the powerOfN function, you will get a type error from the TypeScript compiler/ extension. This can help catch bugs early on and make your code more predictable and easier to understand.
Interfaces
An interface in TypeScript defines a contract that specifies the shape of an object. You can use interfaces to define the expected structure of objects passed as arguments or returned from functions. For example, you can define an interface for a React component's props like this:
interface MyProps {
name: string;
age: number;
gender: string;
}
Then, you can use this interface to type-check the props passed to your component:
import React from 'react';
const MyComponent: React.FC<MyProps> = (props) => {
return (
<div>
<p>Name: {props.name}</p>
<p>Age: {props.age}</p>
<p>Gender: {props.gender}</p>
</div>
);
};
This can be especially useful when working with React components, as you can use interfaces to specify the props and state types for a component.
P.S don't ask me for class-based component examples (get some professional help)
Generics
Generics allow you to write code that can work with a variety of types, rather than being tied to a specific type. This can be useful when writing reusable utility functions or higher-order components
For example, you can use generics to create a higher-order component that can wrap any component and provide it with additional props or functionality. Here's an example of a higher-order component that uses generics to accept a component and pass it a title prop:
import * as React from 'react';
interface Props<T> {
component: React.ComponentType<T>;
title: string;
}
function withTitle<T>(props: Props<T>) {
const { component: Component, title, ...rest } = props;
return (
<div>
<h1>{title}</h1>
<Component {...rest} />
</div>
);
}
export default withTitle;
To use this higher-order component, you can provide it with a component and a title prop:
import * as React from 'react';
import withTitle from './withTitle';
function MyComponent(props: { message: string }) {
return <div>{props.message}</div>;
}
const WrappedComponent = withTitle<{ message: string }>({
component: MyComponent,
title: 'Hello World',
message: 'Hello, world!'
});
export default WrappedComponent;
In this example, the withTitle higher-order component uses a generic type argument to specify the type of props that the wrapped component expects. This allows you to reuse the withTitle component with different components that have different prop types.
Extending interfaces when needed
Let's assume you are using a very popular component library, and you are asked to build a new design system on top of this. for these kinds of situations, you don't really need to create a new interface for each component that you are basically wrapping up right? (because that'll be harmful for your health trust me on that ) this is a very good use-case where you probably want to extend the existing types exposed by the library itself or even just use that same type (all popular libraries do that if yours isn't please re-evaluate your life choices..lol just kidding ... or Am I?), something like this:
import { Button } from "antd";
import { BaseButtonProps } from "antd/lib/button/button";
import React from "react";
interface MyCustomButtonProps extends BaseButtonProps {
extraPropsThatYouWant: any;
}
const MyCustomButton: React.FC<MyCustomButtonProps> = ({
extraPropsThatYouWant,
className = "",
...rest
}) => {
if (extraPropsThatYouWant) {
// do something
}
return (
<Button
{...rest}
className={`${className} some-random-custom-classes-designed-by-your-company `}
/>
);
};
export default MyCustomButton;
This kind of code will not only save you a massive amount of time but also give you the type-safety for almost all kinds of props that you probably didn't comprehend
Type Aliases
Type aliases allow you to define a new name for a type. This can be useful when you want to use a complex type multiple times, or when you want to give a more descriptive name to a type. For example, you can define a type alias for a list of strings like this:
type StringList = string[];
You can then use the StringList type alias wherever you would use an array of strings:
const names: StringList = ['Alice', 'Bob', 'Charlie'];
This sort of code will not only make your code-base unique but also make an impression that things are written a certain way and they need to stay that way!! to anyone that tries to contribute to your repo
Mapped Types
Mapped types allow you to create a new type by applying a transformation to the properties of an existing type. For example, you can create a type that maps all the optional properties of an object to required properties like this:
type RequiredProps<T> = {
[P in keyof T]-?: T[P];
};
You can then use the RequiredProps type to create a new type with all the optional properties of an existing type converted to required properties:
type Props = {
name?: string;
age?: number;
occupation?: string;
};
const requiredProps: RequiredProps<Props> = {
name: 'Alice',
age: 30,
occupation: 'Software Developer',
};
Decorators
Decorators are a way to annotate and modify classes, methods, and properties at design time. They can be used to add additional functionality to your code, such as adding a logging feature or adding a debouncing function to a method. In React, you can use decorators to add additional functionality to your components, such as connecting them to a Redux store.
To use decorators in TypeScript, you need to enable the experimentalDecorators and emitDecoratorMetadata options in your tsconfig.json file( this might change depending on when you are trying this). Then, you can define a decorator like this:
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`${key} method called with arguments: ${args}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
You can then apply the decorator to a class method like this:
class MyClass {
@log
myMethod(arg1: string, arg2: number) {
// method implementation
}
}
Axios with typescript
You would not believe how many junior/senior devs alike I have seen get this thing wrong, and let's be honest Axios is the go-to network lib in your repo if you are using typescript.
Here is an example of using TypeScript with Axios to type-check the request and response data for a GET request:
import axios from 'axios';
interface User {
id: number;
name: string;
email: string;
}
async function getUsers(): Promise<User[]> {
const response = await axios.get<User[]>('/api/users');
return response.data;
}
In this example, the getUsers function makes a GET request to the /api/users endpoint and expects an array of User objects in the response. The axios.get method is called with the User[] type parameter, which specifies the type of the response data.
You can also use TypeScript to define the types of the Axios config and response objects. For example:
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
interface User {
id: number;
name: string;
email: string;
}
async function createUser(user: User): Promise<User> {
const config: AxiosRequestConfig = {
method: 'POST',
url: '/api/users',
data: user,
};
const response: AxiosResponse<User> = await axios(config);
return response.data;
}
In this example, the createUser function makes a POST request to the /api/users endpoint with a User object as the request data. The AxiosRequestConfig type is used to define the type of the config object passed to the axios function, and the AxiosResponse type is used to define the type of the response object.
By using TypeScript with Axios, you can ensure that your request and response data are correctly typed and that you have access to the necessary Axios objects. This can help catch bugs early on and make your code more predictable and easier to understand.
Advanced Util Types
TypeScript has several advanced types that allow you to express complex type relationships in your code. Some examples of these advanced types include:
Partial: Constructs a type with all properties of Type set to optional. This utility will return a type that represents all subsets of a given type.
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "organize desk",
description: "clear clutter",
};
const todo2 = updateTodo(todo1, {
description: "throw out trash",
});
Readonly: Constructs a type with all properties of Type set to read-only, meaning the properties of the constructed type cannot be reassigned.
interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Delete inactive users",
};
todo.title = "Hello";
Cannot assign to 'title' because it is a read-only property.
Pick: Constructs a type by picking the set of properties Keys (string literal or union of string literals) from Type.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
todo;
Omit:Constructs a type by picking all properties from Type and then removing Keys (string literal or union of string literals).
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
createdAt: 1615544252770,
};
todo;
const todo: TodoPreview
type TodoInfo = Omit<Todo, "completed" | "createdAt">;
const todoInfo: TodoInfo = {
title: "Pick up kids",
description: "Kindergarten closes at 5pm",
};
todoInfo;
There are many more that'll make your life so much easier as a Frontend dev refer to This doc for more details.
Use the Generics system provided by React as much as possible (especially for hooks)
Some notable examples are as follows
- useState:
import React, { useState } from 'react';
const Example = () => {
const [count, setCount] = useState<number>(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
};
- useContext:
import React, { useContext } from 'react';
interface Theme {
backgroundColor: string;
color: string;
}
interface ThemeContext {
theme: Theme;
toggleTheme: () => void;
}
const defaultTheme: Theme = {
backgroundColor: 'white',
color: 'black'
};
const ThemeContext = React.createContext<ThemeContext>({
theme: defaultTheme,
toggleTheme: () => {}
});
const Example = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme.backgroundColor, color: theme.color }}>
<p>Hello World!</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
};
- useReducer:
import React, { useReducer } from 'react';
interface State {
count: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' };
const reducer = (state: State, action: Action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
};
const Example = () => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
};
Contrary to popular beliefs use "as" when you know more than the type-system
There will be times when you face a situation where either typescript inference is just wrong or the library you are trying to use doesn't have a robust types export, in those cases (Only in those cases) you are absolutely allowed to use the "as" keyword because you know better than typescript .
A great example would be the use of "Object.keys", by default the type of the output is an array of strings which is not wrong , but for example you need to restrict it to only the keys available in your object , in that case you have to use as, because in this case you know more than typescript.
const obj={
key1:"some value",
key2:"some value"
}
const keys = Object.keys(obj) as Array<keyof typeof obj>
In this case the type of keys would be
const keys: ("key1" | "key2")[]
Feel free to follow me on other platforms as well
Top comments (5)
nice, thanks, one question:
in this example:
type RequiredProps = {
[P in keyof T]-?: T[P];
};
what does the -? do? I guess it somehow gets the optional ones but I never saw -? construct
found it: You can remove or add the modifiers by prefixing with - or +. If you don’t add a prefix, then + is assumed. ;)
tesst
Hi,
Could you recommend a good course /project to learn typescript with react and node?
So one way would be to actually going over the official typescript docs very fast , you don't have to remember every single details but just a high level picture would be nice . and a really nice trick to learning typescript with React is to try and contribute to a React component library that's fully type safe . you would be surprised by how intricate the code for some major React component libraries are . best of luck