The strict Flag: Your Best Friend

In your tsconfig.json, the single most impactful setting is "strict": true. This option is not just one rule; it's a suite of stricter type-checking rules that help you catch a vast range of common bugs.

One of the most important rules it enables is strictNullChecks. Without it, null and undefined are assignable to every type, which is a massive source of runtime errors (e.g., "Cannot read property 'x' of undefined").

TypeScript


// With "strictNullChecks": false (the default)
let name: string = "Alice";
name = null; // This is allowed, but will likely cause an error later.

// With "strictNullChecks": true
let name: string = "Alice";
// name = null; // ERROR: Type 'null' is not assignable to type 'string'.

// You must be explicit if a value can be null or undefined
let nullableName: string | null = "Bob";
nullableName = null; // This is now okay!

Enabling strict mode forces you to handle potential null and undefined values explicitly, making your code far more robust.

Type Narrowing: How TypeScript Gets Smarter

TypeScript's compiler is smart enough to analyze your code's control flow. It can narrow a variable from a broad type to a more specific one within a certain block of code.

TypeScript


function printLength(value: string | number) {
  // At this point, 'value' could be a string OR a number.
  
  if (typeof value === 'string') {
    // Inside this block, TS knows 'value' MUST be a string.
    console.log(value.toUpperCase()); // This is safe.
  } else {
    // Inside this block, TS knows 'value' MUST be a number.
    console.log(value.toFixed(2)); // This is safe.
  }
}

This works with common JavaScript operators like typeof, instanceof, and property checks.

Discriminated Unions: A Powerful Pattern

You can supercharge type narrowing with a pattern called discriminated unions. This involves creating a common property (the "discriminant") on each type in a union, which has a unique literal string value.

TypeScript


interface Circle {
  kind: "circle"; // The discriminant
  radius: number;
}

interface Square {
  kind: "square"; // The discriminant
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // TS knows 'shape' is a Circle here.
      return Math.PI * shape.radius ** 2;
    case "square":
      // TS knows 'shape' is a Square here.
      return shape.sideLength ** 2;
  }
}

By checking the kind property, TypeScript can perfectly narrow the shape variable to the correct type within each case block.

User-Defined Type Guards

Sometimes, TypeScript needs a little help. You can create your own special function, called a type guard, that performs a check and tells the compiler about the type of a variable.

A type guard is a function whose return type is a type predicate: argument is Type.

TypeScript


interface Fish {
  swim(): void;
}
interface Bird {
  fly(): void;
}

// This is a user-defined type guard.
// Its return type 'pet is Fish' tells TS that if this function returns true,
// the 'pet' argument should be treated as a 'Fish'.
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    // Because isFish returned true, TS now knows 'pet' is a Fish.
    pet.swim();
  } else {
    // And here it must be a Bird.
    pet.fly();
  }
}

This is an advanced but incredibly powerful feature for working with complex types.

Quizzes

1. What is the primary benefit of enabling "strictNullChecks": true (which is part of "strict": true) in tsconfig.json?

  • A) It makes your code compile faster.
  • B) It prevents you from using the values null and undefined anywhere in your code.
  • C) It forces you to explicitly handle cases where a variable might be null or undefined, preventing common runtime errors.
  • D) It automatically converts all null values to empty strings.

Answer: C Explanation: strictNullChecks makes null and undefined their own distinct types. They can no longer be assigned to variables of other types (like string or number) by default. This forces the developer to use union types (e.g., string | null) and perform explicit checks, which eliminates a massive category of bugs.

2. You have a function that accepts a value of type unknown. Inside the function, you have an if (typeof value === 'number') { ... } block. What does TypeScript know about value inside this block?

  • A) value is still of type unknown.
  • B) value is now of type any.
  • C) TypeScript has narrowed the type of value to number.
  • D) The code will cause a compiler error.

Answer: C Explanation: This is a classic example of type narrowing. TypeScript's control flow analysis understands the typeof check. Within the scope of the if block, it intelligently narrows the type of value from the broad unknown to the specific number, allowing you to safely call number-specific methods on it.

Tutorial 6: TypeScript — Migrate JS to TS best practices

  • Difficulty: Intermediate
  • Description: Ready to bring the power of types to an existing JavaScript project? Don't try to boil the ocean! This guide provides a practical, step-by-step strategy for migrating a codebase from JavaScript to TypeScript gradually, safely, and without halting feature development. 🛤️
  • Time to Read: 15 minutes

Content

The Golden Rule: Migrate Gradually

The biggest mistake you can make is trying to convert a large JavaScript project to TypeScript all at once. This leads to a massive, unmanageable pull request and brings all other development to a halt. The key to a successful migration is to do it incrementally.

Step 1: The Setup

First, introduce TypeScript into your project and configure it to coexist peacefully with your existing JavaScript.

  1. Install TypeScript: npm install typescript @types/node @types/react --save-dev (install types for your environment, e.g., Node, React).
  2. Create tsconfig.json: Run tsc --init.
  3. Configure for Coexistence: This is the most critical part. Modify your tsconfig.json:
  4. JSON

{
  "compilerOptions": {
    // --- Crucial for migration ---
    "allowJs": true,     // Allow JavaScript files to be compiled.
    "checkJs": false,    // (Optional) Don't type-check JS files yet.
    "noImplicitAny": false, // Start loose, tighten later.

    // --- Good defaults ---
    "target": "es6",
    "module": "commonjs",
    "strict": false, // You'll enable this later!
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["./src"] // Tell TS where your source code is.
}
  1. The "allowJs": true option is essential. It tells the TypeScript compiler to include .js files in its compilation process.
Step 2: Start with the "Leaves"

Where do you start converting files? In your project's dependency graph, start with the files that have few or no dependencies. These are often called "leaf" nodes.

Good candidates are utility functions, constants, or simple UI components. By starting here, you avoid complex type errors that cascade from un-typed dependencies.

Step 3: The Conversion Process (File by File)

For each file you choose to convert:

  1. Rename the file: Change the extension from .js to .ts (or .tsx for React components).
  2. Fix the Initial Errors: The TypeScript compiler will immediately show you some errors. Don't be intimidated! Many of these will be "implicit any" errors where it can't figure out a type.
  3. Add Explicit Types: Start by adding types to function parameters and return values. This provides the most value. Create interface or type definitions for complex objects.
  4. TypeScript

// BEFORE (in JS)
// function calculateTotal(items) { ... }

// AFTER (in TS)
interface CartItem {
  name: string;
  price: number;
}
function calculateTotal(items: CartItem[]): number { ... }
  1. Use any as an Escape Hatch (Sparingly!): If you're blocked by a complex type from a third-party library or another part of your code that isn't typed yet, it's okay to use any as a temporary measure to get the code compiling. Add a comment to come back to it later.
  2. TypeScript

// A temporary fix to unblock yourself
function handleLegacyData(data: any) { // TODO: Create a proper type for this data
  // ...
}
Step 4: Tighten the Screws

Once a significant portion of your codebase has been converted, you can start making your TypeScript configuration stricter. In your tsconfig.json, begin enabling stricter checks. The ultimate goal is to get to:

JSON


"strict": true

This process is a marathon, not a sprint. Celebrate small wins, convert file by file, and slowly increase type coverage and strictness across your project.