In a full - stack application, using TypeScript on both the front - end (e.g., with React or Angular) and the back - end (e.g., with Node.js and Express) allows for seamless data flow. TypeScript type definitions can be shared between the two layers. For example, if you have a user object that is sent from the back - end to the front - end, you can define the User
type in a shared module.
// shared/types.ts
export interface User {
id: number;
name: string;
email: string;
}
TypeScript’s type system is very powerful. Concepts like mapped types, conditional types, and intersection types can be used to create more complex and precise types. For instance, a mapped type can be used to create a new type based on an existing one.
// Mapped type example
type ReadonlyUser<T> = {
readonly [P in keyof T]: T[P];
};
type ReadonlyUserType = ReadonlyUser<User>;
To share type definitions between the front - end and back - end, you can use a monorepo structure. Tools like Lerna or Yarn Workspaces can help manage multiple packages within a single repository.
mkdir shared
cd shared
yarn init -y
shared/types.ts
as shown above.yarn add file:../shared
// front - end code
import { User } from '../shared/types';
const user: User = {
id: 1,
name: 'John Doe',
email: '[email protected]'
};
When using advanced type system features, you need to understand the syntax and semantics. For example, conditional types can be used to select a type based on a condition.
type IsString<T> = T extends string? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
Use TypeScript types to handle errors more gracefully. For example, you can define a type for different error cases.
// Error types
type DatabaseError = {
type: 'database';
message: string;
};
type NetworkError = {
type: 'network';
message: string;
};
type AppError = DatabaseError | NetworkError;
function handleError(error: AppError) {
if (error.type === 'database') {
console.log('Database error:', error.message);
} else if (error.type === 'network') {
console.log('Network error:', error.message);
}
}
When making API calls, use TypeScript to ensure type safety. You can use libraries like Axios with TypeScript.
import axios from 'axios';
import { User } from '../shared/types';
async function getUser(id: number): Promise<User> {
const response = await axios.get<User>(`/api/users/${id}`);
return response.data;
}
Avoid creating overly complex types. If a type becomes too hard to understand, break it down into smaller, more manageable types.
Type assertions should be used only when you are absolutely sure about the type. Overusing type assertions can bypass TypeScript’s type checking and lead to runtime errors.
Although TypeScript is a compile - time type system, you can write unit tests to ensure that your types are working as expected. Tools like Jest can be used for this purpose.
import { IsString } from './types';
describe('IsString type', () => {
it('should return true for string type', () => {
type Result = IsString<string>;
expect<Result>(true as Result).toBe(true);
});
it('should return false for non - string type', () => {
type Result = IsString<number>;
expect<Result>(false as Result).toBe(false);
});
});
Double TypeScript, whether it means sharing types across the front - end and back - end or leveraging advanced type system features, offers significant benefits in terms of code quality, maintainability, and type safety. By understanding the fundamental concepts, using the right usage methods, following common practices, and adhering to best practices, developers can make the most out of TypeScript in their projects.