These take you deeper into mastering TypeScriptโs type system and writing safe, complex applications:
- Advanced Types
- Generics
- Declaration Files (
.d.ts) - Modules and Namespaces
- Type Operators:
keyof,typeof,in,infer - Discriminated Unions
- Recursive Types
- Handling
thisin Different Contexts - TypeScript with React (JSX/TSX & Props Typing)
- Working with 3rd Party Libraries (
@types) - Configuring
tsconfig.jsonfor Real-World Projects - Type Checking vs Runtime Checking
- Best Practices & Clean Code with TypeScript
Mapped types transform existing types into new ones.
type User = {
name: string;
age: number;
};
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};- Use Case: Creating variations of existing types (e.g.,
Readonly,Partial).
Conditional types allow type logic based on conditions.
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false- Use Case: Creating types that depend on other types.
Access a typeโs properties using an index.
type User = { name: string; age: number };
type NameType = User["name"]; // string- Use Case: Extracting specific property types.
Combine string literals with types.
type Route = `/users/${string}`;
const route: Route = "/users/123"; // Valid- Use Case: Defining dynamic string patterns.
Generics provide a way to create reusable, type-safe components.
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42);
const str = identity<string>("Hello");- Use Case: Writing reusable functions with type safety.
interface Box<T> {
value: T;
}
const box: Box<number> = { value: 42 };- Use Case: Creating flexible data structures.
Restrict the types that can be used with generics.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("Hello"); // 5
getLength([1, 2, 3]); // 3- Use Case: Enforcing specific properties on generic types.
Declaration files provide type definitions for JavaScript libraries.
// math.d.ts
declare module "math" {
export function add(a: number, b: number): number;
}- Use Case: Adding type definitions for libraries without built-in TypeScript support.
Modules use import and export to organize code.
// utils.ts
export function greet(name: string): string {
return `Hello, ${name}`;
}
// main.ts
import { greet } from "./utils";
console.log(greet("Alice"));Namespaces group related code under a single name.
namespace Utils {
export function greet(name: string): string {
return `Hello, ${name}`;
}
}
console.log(Utils.greet("Alice"));- Use Case: Namespaces are useful for organizing code in non-modular environments.
Extracts the keys of a type.
type User = { name: string; age: number };
type UserKeys = keyof User; // "name" | "age"Gets the type of a value.
const user = { name: "Alice", age: 25 };
type UserType = typeof user; // { name: string; age: number }Iterates over keys in a type.
type ReadonlyUser = {
[K in keyof User]: User[K];
};Infers a type within a conditional type.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Test = ReturnType<() => string>; // stringDiscriminated unions simplify working with multiple object types.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.side ** 2;
}
}Recursive types allow types to reference themselves.
type NestedArray<T> = T | NestedArray<T>[];
const arr: NestedArray<number> = [1, [2, [3]]];- Use Case: Defining deeply nested structures.
TypeScript provides better control over this.
class Counter {
count = 0;
increment(this: Counter): void {
this.count++;
}
}
const counter = new Counter();
counter.increment();- Use Case: Preventing incorrect
thisusage.
TypeScript enhances React development with type safety.
type ButtonProps = {
label: string;
onClick: () => void;
};
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);const [count, setCount] = React.useState<number>(0);Use @types packages for type definitions.
npm install --save-dev @types/lodashimport _ from "lodash";
_.chunk([1, 2, 3, 4], 2); // [[1, 2], [3, 4]]{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}TypeScript performs type checking at compile time, but runtime errors can still occur.
function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero");
return a / b;
}- Use Case: Combine TypeScript with runtime checks for robust applications.
- Enable Strict Mode: Use
"strict": trueintsconfig.json. - Use Type Inference: Let TypeScript infer types when possible.
- Avoid
any: Useunknownor proper types instead. - Use Utility Types: Simplify type transformations.
- Write Declaration Files: Add types for external libraries.