DEV Community

Cover image for Things about typescript you should know as a pro React dev
Akashdeep Patra
Akashdeep Patra

Posted on

 

Things about typescript you should know as a pro React dev

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);
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
};

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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[];
Enter fullscreen mode Exit fullscreen mode

You can then use the StringList type alias wherever you would use an array of strings:

const names: StringList = ['Alice', 'Bob', 'Charlie'];

Enter fullscreen mode Exit fullscreen mode

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];
};

Enter fullscreen mode Exit fullscreen mode

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',
};

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

You can then apply the decorator to a class method like this:

class MyClass {
  @log
  myMethod(arg1: string, arg2: number) {
    // method implementation
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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>
  );
};

Enter fullscreen mode Exit fullscreen mode
  • 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>
  );
};

Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
whiteadi profile image
Adrian Albu

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

Collapse
 
whiteadi profile image
Adrian Albu

found it: You can remove or add the modifiers by prefixing with - or +. If you don’t add a prefix, then + is assumed. ;)

Collapse
 
tainguyen1501 profile image
tainguyen1501

tesst

Collapse
 
apurvjain28 profile image
apurvjain28

Hi,
Could you recommend a good course /project to learn typescript with react and node?

Collapse
 
mr_mornin_star profile image
Akashdeep Patra

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

Visualizing Promises and Async/Await 🀯

async await

☝️ Check out this all-time classic DEV post