# TypeScript for React Developers Series: Practical Tips and Real-World Scenarios

Switching to TypeScript can significantly enhance your React development experience by making your code more maintainable and scalable. With TypeScript’s powerful type-checking, you’ll catch bugs early, enjoy better code navigation, and find refactoring easier. In this article, we’ll dive into practical tips and real-world scenarios to help you effectively adopt TypeScript in your React projects. Whether you’re new to TypeScript or looking to refine your skills, this guide has something for you. Let’s get started!

## **Prerequisites:**

* Familiarity with basic TypeScript types
    
* Familiarity with React.js
    
* Reading the [**previous article**](https://blog.othmane.me/typescript-for-react-developers-series-how-to-get-started) is recommended
    

TypeScript provides several built-in utilities that help reduce type duplication, making your code cleaner and easier to maintain. Let’s explore some of these utilities and see how they can be applied in real-world scenarios.

## **For Objects:**

TypeScript provides utilities to avoid duplicating type definitions for objects. Let’s start with a basic object and apply some of these utilities!

```typescript
type User = {
  email: string;
  first_name: string;
  last_name: string;
  username: string;
}
```

### **Pick vs. Omit:**

Sometimes, we need to create a type similar to an existing one but with some properties added or removed. TypeScript provides the `Omit` and `Pick` utilities to make this easy:

```typescript
type User = {
  email: string;
  first_name: string;
  last_name: string;
  username: string;
}

// Create new type without email and username:
type UserWithoutEmailUsername = Omit<User, 'email' | 'username'>
// Result:
// type UserWithoutEmailUsername = {
//   last_name: string;
//   first_name: string;
// }
```

In this example, we use `Omit` to exclude `email` and `username` from the `User` type.

```typescript
type User = {
  email: string;
  first_name: string;
  last_name: string;
  username: string;
}

// Create new type with only email and username:
type UserWithEmailUsername = Pick<User, 'email' | 'username'>
// Result:
// type UserWithEmailUsername = {
//   email: string;
//   username: string;
// }
```

Here, we use `Pick` to include only `email` and `username` from the `User` type.

So, when should you use `Pick` or `Omit`? It depends on the type you want to manipulate and how you want to modify it.

### **Required vs. Partial:**

Let’s consider a real scenario: you’re building an API application with endpoints for creating and updating resources. You need different types for each service function. Your code might look like this:

```typescript
type User = {
    email: string;
    first_name: string;
    last_name: string;
    username: string;
}

function createUser(user: User) {
    // ....
    return {
        email: user.email,
        username: user.username,
        id: 10
    }
}
type OptionalUser = {
    email?: string;
    first_name?: string;
    last_name?: string;
    username?: string;
}
// You need an ID to query the user that you want to update
function updateUser(user: OptionalUser, id: number) {
    // ....
    return {
        email: user.email,
        username: user.username,
        id: 10
    }
}
```

But you can simplify this using `Partial`, making your code cleaner:

```typescript
type User = {
    email: string;
    first_name: string;
    last_name: string;
    username: string;
}

function createUser(user: User) {
    // ....
    return {
        email: user.email,
        username: user.username,
        id: 10
    }
}
function updateUser(user: Partial<User>, id: number) {
    // ....
    return {
        email: user?.email,
        username: user?.username,
        id: 10
    }
}
```

This makes your code easier to understand and more consistent.

What if you have a type with optional properties, and you want to clone this type but make all properties required? TypeScript provides the `Required` utility:

```typescript
type OptionalUser = {
    email?: string;
    first_name?: string;
    last_name?: string;
    username: string;
}

type RequiredUser = Required<OptionalUser>
```

### **Readonly:**

When working with React, the props passed to a component shouldn’t be modified within the component to enforce immutability. TypeScript provides the `readonly` modifier to prevent reassignment of props, supporting this best practice. [React props should be read-only](https://rules.sonarsource.com/typescript/RSPEC-6759/).

```typescript
import React from 'react';
// Define the props interface with readonly
interface UserCardProps {
  readonly name: string;
  readonly age: number;
}const UserCard: React.FC<UserCardProps> = ({ name, age }) => {
  // Attempting to modify name or age will cause a TypeScript error
  // name = "New Name"; // Error: Cannot assign to 'name' because it is a read-only property.  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
    </div>
  );
};export default UserCard;
```

### **Record:**

The `Record` utility is a powerful tool to create objects with specific key-value pairs, ensuring that all keys are present and that each key's value matches a defined type. Let's consider a real-world scenario where using the `Record` utility type would be beneficial. Imagine you are developing an admin dashboard for a web application where users have different roles, such as `admin`, `editor`, and `viewer`. Each role has specific permissions associated with it, such as the ability to `create`, `read`, `update`, or `delete` data. You can use the `Record` type to define and manage these roles and their permissions in a type-safe way.

**Define User Roles and Permissions:**

```typescript
// Define possible roles
type UserRole = 'admin' | 'editor' | 'viewer';

// Define possible permissions
type Permission = 'create' | 'read' | 'update' | 'delete';
// Use Record to map roles to their permissions
const rolePermissions: Record<UserRole, Permission[]> = {
  admin: ['create', 'read', 'update', 'delete'],
  editor: ['create', 'read', 'update'],
  viewer: ['read'],
};
```

**Creating a Function to Check Permissions:**

```typescript
function hasPermission(role: UserRole, permission: Permission): boolean {
  const permissions = rolePermissions[role];
  return permissions.includes(permission);
}
```

**Using the Function in a Component:**

```typescript
import React from 'react';
type UserProps = {
  role: UserRole;
};
const Dashboard: React.FC<UserProps> = ({ role }) => {
  return (
    <div>
      <h1>Dashboard</h1>
      {hasPermission(role, 'create') && (
        <button>Create New Item</button>
      )}
      {hasPermission(role, 'read') && (
        <div>Here are the items you can view.</div>
      )}
      {hasPermission(role, 'update') && (
        <button>Update Item</button>
      )}
      {hasPermission(role, 'delete') && (
        <button>Delete Item</button>
      )}
    </div>
  );
};
export default Dashboard;
```

**Example Usage of the Dashboard Component:**

```typescript
import React from 'react';
import Dashboard from './Dashboard';

const App: React.FC = () => {
  return (
    <div>
      <h1>Admin View</h1>
      <Dashboard role="admin" />
      <h1>Editor View</h1>
      <Dashboard role="editor" />
      <h1>Viewer View</h1>
      <Dashboard role="viewer" />
    </div>
  );
};
export default App;
```

### **NonNullable:**

Let’s consider a scenario where you have a form for managing user profiles. This form can be used to create a new user or edit an existing one. We’ll use TypeScript to handle these scenarios safely, ensuring that certain fields are non-nullable when the form is submitted.

**Defining the User Data Type:**

```typescript
interface User {
  id?: number; // Optional during creation, required during editing
  username: string;
  email: string;
  age?: number;
}
```

**Form State Type:**

```typescript
type UserFormState = {
  id?: number;
  username: string | undefined;
  email: string | undefined;
  age?: number | undefined;
};
```

This type allows fields like `username` and `email` to be `undefined` initially, especially when editing.

**Using** `NonNullable` **to Ensure Non-Nullable Fields**

When submitting the form, you want to make sure that `username` and `email` are always defined:

```typescript
type ValidUser = Omit<User, 'id'> & {
  id?: number;
  username: NonNullable<UserFormState['username']>;
  email: NonNullable<UserFormState['email']>;
};
```

Here, `ValidUser` ensures that `username` and `email` are non-nullable, making sure

these fields are always present when processing the form submission.

**Form Component Implementation:**

```typescript
import React, { useState } from 'react';

const UserForm: React.FC<{ initialData?: User }> = ({ initialData }) => {
  // Initialize form state with optional initial data
  const [formState, setFormState] = useState<UserFormState>({
    id: initialData?.id,
    username: initialData?.username,
    email: initialData?.email,
    age: initialData?.age,
  });
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setFormState((prevState) => ({ ...prevState, [name]: value }));
  };
  const handleSubmit = () => {
    // Assert that username and email are non-nullable
    const { id, username, email, age } = formState;
    if (!username || !email) {
      alert('Username and email are required.');
      return;
    }
    const validUser: ValidUser = {
      id,
      username,
      email,
      age,
    };
    // Proceed with form submission logic
    console.log('Submitting form:', validUser);
  };
  return (
    <form onSubmit={(e) => e.preventDefault()}>
      <div>
        <label>
          Username:
          <input
            type="text"
            name="username"
            value={formState.username || ''}
            onChange={handleChange}
            required
          />
        </label>
      </div>
      <div>
        <label>
          Email:
          <input
            type="email"
            name="email"
            value={formState.email || ''}
            onChange={handleChange}
            required
          />
        </label>
      </div>
      <div>
        <label>
          Age:
          <input
            type="number"
            name="age"
            value={formState.age || ''}
            onChange={handleChange}
          />
        </label>
      </div>
      <button type="submit" onClick={handleSubmit}>
        Submit
      </button>
    </form>
  );
};
export default UserForm;
```

## **For Functions:**

TypeScript gives us many utilities to work with functions, allowing us to infer types and avoid duplicating type definitions. Let’s dive into an example:

```typescript
type User = {
  email: string;
  first_name: string;
  last_name: string;
  username: string;
}

function createUser(args: User) {
    // ....
    return {
        id: 12,
        email: args.email,
        username: args.username
    }
}
```

**How can we infer the return type of this function?**

We have two methods to get the return type of this function:

```typescript
type returnTypeOfCreateUser = Omit<User, 'last_name' | 'first_name'>
```

This approach works, but it doesn’t offer the flexibility or consistency needed if the return type of the function changes. To solve this, we can use `ReturnType` to directly infer the type:

```typescript
type returnTypeOfCreateUser2 = ReturnType<typeof createUser>
```

**What about async functions?**

```typescript
async function createUser2(args: User) {
    // ....
    return {
        id: 12,
        email: args.email,
        username: args.username
    }
}
```

The answer is yes! You can combine `Awaited` with `ReturnType` to handle async functions:

```typescript
type returnTypeOfCreateUser3 = Awaited<ReturnType<typeof createUser2>>
```

**What if I want to get the type of parameters that a function from an external library accepts?**

Imagine you’re using a third-party library, and you want to wrap one of its functions. You can use the `Parameters` utility to get the types of its parameters:

```typescript
type paramsType = Parameters<typeof createUser> // type paramsType = [args: User]
```

The `Parameters` utility returns an array of the parameter types that `createUser` accepts!

## **Conclusion**

TypeScript’s utility types like `ReturnType`, `Parameters`, `Partial`, `Required`, `Omit`, `Pick`, `Readonly`, `Record`, and `NonNullable` make it easier to write clean, maintainable, and type-safe React code. By using these utilities, you can handle real-world scenarios effectively, reduce errors, and enhance the overall quality of your projects. Start leveraging these powerful features to take your React development to the next level. For more detailed information and advanced use cases, check out additional resources like the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/utility-types.html) and the [React Documentation](https://react.dev/reference/react).
