NIXX/DEVv1.14.0
ArticlesFavorites
Sign In
Sign In
Articles

Welcome to our blog

A curated collection of insightful articles, practical guides, and expert tips designed to simplify your workflow

Cover image for: 10 Things I Wish I Knew Before Learning TypeScript
September 2, 20257 MIN READ min readBy ℵi✗✗

10 Things I Wish I Knew Before Learning TypeScript

TypeScript can feel like a big leap if you’re coming from plain JavaScript. In this article, I share the 10 most important lessons I wish I had known from the start—so you can skip the trial-and-error and write better code faster.

webdevjavascripttypescriptprogramming-tips
ℵi✗✗

ℵi✗✗

Full-Stack Developer

Passionate about building tools and sharing knowledge with the developer community.

Was this helpful?

Popular Posts

  • NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    5 MIN READ min read

  • How to Enable HTTPS on Localhost in Under 2 Minutes

    How to Enable HTTPS on Localhost in Under 2 Minutes

    3 MIN READ min read

  • Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    4 MIN READ min read

  • Array Destructuring in PHP: A Practical Guide for Modern Developers

    Array Destructuring in PHP: A Practical Guide for Modern Developers

    5 MIN READ min read

Recommended Products

  • Apple iPad (7th Gen)

    Apple iPad (7th Gen)

    4.3
  • Fitbit Versa 4

    Fitbit Versa 4

    4.3
  • JBL Flip 6

    JBL Flip 6

    4.8
  • Dell 24 Monitor — SE2425HM Full HD

    Dell 24 Monitor — SE2425HM Full HD

    4.7

May contain affiliate links

Topics

webdev33productivity16cybersecurity12javascript11automation9guide8react7typescript7php6tutorial6freelancing5github actions5privacy5how to4Node.js4
+111 more topics →
🇺🇸USD ACCOUNTOpen a free US-based USD accountReceive & save in USD — powered by ClevaSponsoredInterserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

TypeScript does catch errors before runtime — but it only does so reliably when it is configured and used correctly. The default settings are permissive enough that you can write TypeScript that provides almost no type safety at all. Several common patterns actively undermine the benefits of the type system. And there are a handful of features — as const, never, the exhaustive check pattern — that solve real problems once you know they exist.

These are the ten lessons that would have made the learning curve shorter and produced better code earlier.

What this covers:

  • Strict mode and why to enable it from day one

  • Why any defeats the type system and what to use instead

  • When to annotate and when to trust inference

  • as const for literal types and immutable data

  • Union and intersection types for modeling state

  • Generics without the confusion

  • Built-in utility types worth knowing

  • The never exhaustive check pattern

  • Type checking in CI

  • Why TypeScript does not replace tests


1. Enable Strict Mode from Day One

TypeScript's default configuration is deliberately permissive to ease migration from JavaScript. For a project starting from scratch, this permissiveness teaches habits that require unlearning later.

Enabling "strict": true in tsconfig.json activates the full set of safety checks:

{
    "compilerOptions": {
        "strict": true
    }
}

Strict mode includes strictNullChecks (null and undefined are distinct types), noImplicitAny (all parameters must have types), and several others. These are the checks that prevent the most common categories of runtime errors.

Starting with strict mode means encountering these constraints while the codebase is small. Adding it to an existing large codebase produces hundreds of errors at once.


2. Avoid any — Use unknown Instead

any is an escape hatch from the type system. A variable typed as any accepts any value and provides no type information when it is used. TypeScript stops checking it. This is occasionally necessary during migration, but using it as a default when a type is difficult to express defeats the purpose of the tool.

unknown is the safer alternative. It also accepts any value, but TypeScript requires a type check before it can be used:

function processInput(value: unknown): string {
    if (typeof value === "string") {
        return value.toUpperCase(); // safe to use as string here
    }
    if (typeof value === "number") {
        return value.toFixed(2);
    }
    throw new TypeError(`Unexpected input type: ${typeof value}`);
}

The constraint is the feature: unknown forces explicit handling of each possible type, which is exactly what makes error-prone paths visible.


3. Trust Type Inference

TypeScript infers types from initialisations, function return paths, and the context in which expressions appear. Annotating every variable and function return type manually produces verbose code and, in some cases, works against inference by providing a less specific type than TypeScript would derive on its own.

// Annotation is redundant — TypeScript infers number
let count: number = 0;

// TypeScript infers string without help
const name = "Ada";

// TypeScript infers the return type as boolean | null
function check(value: string) {
    if (value.length > 0) return true;
    return null;
}

Explicit annotations are worth adding where the inferred type is inaccurate, where a function forms part of a public API, or where the type communicates intent that inference does not capture. For local variables and simple functions, let TypeScript do the work.


4. Use as const for Literal Types and Immutable Data

When a value is assigned to a let variable, TypeScript widens the type. "admin" becomes string. 42 becomes number. This is correct for a mutable variable but loses precision for a constant.

as const preserves the literal type:

const roles = ["admin", "user", "guest"] as const;
// Type: readonly ["admin", "user", "guest"]

const config = {
    env: "production",
    port: 3000,
} as const;
// Type: { readonly env: "production"; readonly port: 3000 }

This is particularly useful when the values are used in a union type or as keys in a Record. Without as const, a string array has type string[] and cannot be used to derive a union type of its members. With it:

type Role = typeof roles[number];
// Type: "admin" | "user" | "guest"

5. Model State with Union Types

Multiple boolean flags to represent a single piece of state is a pattern that TypeScript's union types replace cleanly. A loading state with separate isLoading, isError, and isSuccess booleans allows invalid combinations — isLoading: true and isSuccess: true simultaneously, for instance.

A discriminated union makes invalid states unrepresentable:

type RequestState =
    | { status: "idle" }
    | { status: "loading" }
    | { status: "success"; data: User[] }
    | { status: "error"; message: string };

The data field only exists on the success state. The message field only exists on the error state. TypeScript enforces this in every switch or conditional that handles the union.

Intersection types compose multiple types into one:

type WithTimestamps = {
    createdAt: Date;
    updatedAt: Date;
};

type UserRecord = User & WithTimestamps;

6. Generics Are Just Parameterized Types

Generics look intimidating at first but they are a straightforward concept: a type parameter that lets a function or type work with different concrete types without losing type information.

function first<T>(arr: T[]): T | undefined {
    return arr[0];
}

const num = first([1, 2, 3]);       // T is number, result is number | undefined
const str = first(["a", "b"]);     // T is string, result is string | undefined

Without generics, first would need to return any (losing type information) or be written separately for each type (defeating reuse). With generics, one function works for all types and TypeScript knows the specific return type at each call site.

Constraints narrow what T can be:

function getLength<T extends { length: number }>(item: T): number {
    return item.length;
}

getLength("hello");    // works — string has length
getLength([1, 2, 3]);  // works — array has length
getLength(42);         // error — number has no length

7. Learn the Built-In Utility Types

TypeScript ships with a set of utility types that transform or compose existing types without requiring manual interface definitions. The ones worth knowing first:

  • Partial<T> — makes all fields optional

  • Required<T> — makes all fields required

  • Pick<T, K> — creates a type with only the specified keys

  • Omit<T, K> — creates a type with the specified keys removed

  • Readonly<T> — prevents reassignment to any field

  • Record<K, V> — maps a set of keys to a value type

  • ReturnType<F> — extracts the return type of a function

type UserUpdate = Partial<Omit<User, "id">>;
// All User fields optional, id excluded — a standard patch payload type

These types compose. A few combined produce precize transformations that would otherwize require separate interface definitions.


8. Use never for Exhaustive Checks

never is the type that represents an unreachable value — a value that should never exist at a given point in the code. Its most practical use is in exhaustive switch statements over a union type.

type Status = "loading" | "success" | "error";

function handleStatus(status: Status): string {
    switch (status) {
        case "loading":
            return "Loading...";
        case "success":
            return "Done";
        case "error":
            return "Something went wrong";
        default:
            const _check: never = status;
            throw new Error(`Unhandled status: ${_check}`);
    }
}

If a new value is added to the Status union without updating this function, the assignment const _check: never = status produces a type error. TypeScript knows that status can no longer be never at that point because there is an unhandled case.

This pattern ensures that adding to a union type surfaces every place in the codebase that needs to be updated.


9. Run Type Checking in CI

Editor integration gives immediate feedback while writing code, but it is not a reliable gate. TypeScript errors in files that are not currently open are not visible. A developer in a hurry may ignore or suppress a warning. CI runs tsc against the entire codebase and fails the build if there are any errors.

npx tsc --noEmit

The --noEmit flag runs the type checker without producing output files, which is appropriate for a CI check. Add it to the test or build step so type errors block deployment:

// package.json
"scripts": {
    "typecheck": "tsc --noEmit",
    "ci": "npm run typecheck && npm test"
}

Pairing tsc --noEmit with eslint-plugin-react-hooks or other ESLint rules covering code quality concerns that TypeScript does not address gives a thorough static analysis step in CI.


10. TypeScript Does Not Replace Tests

TypeScript eliminates an entire category of bugs — type mismatches, incorrect property access, null reference errors in typed paths. It does not verify that the logic is correct.

A function that correctly adds two numbers according to TypeScript's type system can still return the wrong result. An API handler with perfect types can still apply business rules incorrectly. A component with fully typed props can still render the wrong content for a given state.

Unit tests verify that functions produce the expected output for a given input. Integration tests verify that components interact correctly. End-to-end tests verify that user workflows complete successfully. TypeScript and tests address different failure modes and both are necessary.


Key Takeaways

  • Enable "strict": true in tsconfig.json from the start. The constraints it enforces are what make TypeScript useful.

  • Use unknown instead of any when a type is genuinely unknown. It requires explicit type checks before use, which is the correct behavior.

  • Trust inference for local variables and simple functions. Add explicit annotations for public APIs and where inference would produce an inaccurate type.

  • Use as const to preserve literal types and derive union types from arrays and objects.

  • Model state with discriminated unions rather than boolean flag combinations. Invalid states become unrepresentable.

  • Generics make functions and types reusable without sacrificing type information.

  • Built-in utility types (Partial, Pick, Omit, Record, ReturnType) reduce the need for manual interface definitions.

  • The never exhaustive check pattern surfaces missing switch cases as type errors when a union is extended.

  • Run tsc --noEmit in CI so type errors block deployment regardless of editor behavior.

  • TypeScript prevents type errors. Tests verify correctness. Both are required.


Conclusion

TypeScript's value compounds as a codebase grows. The habits formed early — strict mode on, any avoided, inference trusted, state modeled with unions — produce a codebase that is easier to refactor, easier to extend, and easier for new developers to understand. The habits that undermine it — permissive config, any as a default, annotations that duplicate what inference already knows — produce code that looks typed but provides little of the benefit.

Starting right is significantly easier than correcting later.


A specific TypeScript lesson or pattern that changed how you write code? Share it in the comments.

Topics
webdevjavascripttypescriptprogramming-tips

Discussion

Join the discussion

Sign in to share your thoughts and engage with the community.

Sign In
Loading comments…

Continue Reading

More Articles

View all
Cover image for: Build a Fun Alphabet Reader with TypeScript, Vite & Speech Synthesis API
Jun 27, 20254 MIN READ min read

Build a Fun Alphabet Reader with TypeScript, Vite & Speech Synthesis API

An interactive, educational project for beginners to learn modern frontend development.

Cover image for: Why You Should Use TypeScript in Every JavaScript Project
Jul 23, 20255 MIN READ min read

Why You Should Use TypeScript in Every JavaScript Project

JavaScript gets the job done—but TypeScript helps you write cleaner, safer, and easier-to-maintain code. Here’s why it’s worth using everywhere.

Cover image for: Array Destructuring in PHP: A Practical Guide for Modern Developers
Mar 12, 20255 MIN READ min read

Array Destructuring in PHP: A Practical Guide for Modern Developers

From PHP 7.1 to 8.1—learn how array destructuring simplifies variable assignment, reduces boilerplate, and improves readability in modern PHP development.

Cover image for: Best Web Hosting of 2026 (Honest Picks From Real-World Use)
Jan 1, 20267 MIN READ min read

Best Web Hosting of 2026 (Honest Picks From Real-World Use)

Choosing the right web hosting in 2026 isn't just about price. A breakdown of the best providers, focusing on reliability, performance, and support.

|Made with · © 2026|TermsPrivacy
AboutBlogContact

Free, open-source tools for developers and creators · Community driven