Back to Blog
TypeScriptJavaScriptWeb Development

TypeScript Tips Every JavaScript Developer Should Know

Switching from JavaScript to TypeScript can feel overwhelming at first. These practical tips will help you get productive fast without fighting the type system.

Updated March 10, 2025
TypeScript Tips Every JavaScript Developer Should Know

Let the compiler do more work than you think it can

TypeScript's type inference is better than most people give it credit for. When you initialize a variable with a value, TypeScript already knows the type — annotating it yourself just adds noise. This applies to function return types too: if the function body is simple and the return type is obvious, you don't need to write it out.

const name: string = "Alice"; // annotation adds nothing
const name = "Alice";         // TypeScript already knows this is string

The places where explicit annotations earn their keep are function parameters (TypeScript can't infer those from the call site), public API boundaries where you want the contract to be explicit, and cases where inference would produce a type that's too wide for your intent.

Utility types exist so you don't duplicate interface definitions

A common pattern in TypeScript codebases is defining multiple interfaces that are variations of the same thing — UserCreate, UserUpdate, UserPublic — all copied from the base User type with some fields added, removed, or made optional. TypeScript's built-in utility types handle all of these cases without copy-paste.

interface User {
    id: number;
    name: string;
    email: string;
    password: string;
}

type UserPreview = Pick<User, "id" | "name">;  // only these fields
type UserPatch   = Partial<User>;               // all fields optional
type PublicUser  = Omit<User, "password">;      // everything except password

The advantage isn't just less code — it's that if you rename a field on User, the derived types update automatically. With manually duplicated interfaces, you'd have to track down every copy.

Discriminated unions give the compiler knowledge about variants

When a value can be one of several shapes, discriminated unions let TypeScript know which shape you're dealing with inside each branch. The pattern requires a shared field with a literal type on each variant — TypeScript narrows on that field to figure out what else is available.

type ApiResult<T> =
    | { ok: true;  data: T        }
    | { ok: false; error: string  };

function handleResult(result: ApiResult<User>) {
    if (result.ok) {
        console.log(result.data.name); // TypeScript knows data exists here
    } else {
        console.error(result.error);   // TypeScript knows error exists here
    }
}

This is particularly useful for API response wrappers. Instead of catching exceptions or checking for undefined, you return a typed result that forces callers to handle both cases.

unknown forces you to think about what you're actually receiving

any shuts off type checking entirely. It's sometimes useful as a temporary escape hatch, but it defeats the purpose of TypeScript when used for things that are genuinely unknown at compile time. unknown is the honest version — it tells TypeScript "this could be anything" while still requiring you to narrow before use.

function parseJSON(input: string): unknown {
    return JSON.parse(input);
}

const data = parseJSON('{"name": "Alice"}');

// TypeScript won't allow this — data is unknown
console.log(data.name);

// You need to narrow first
if (typeof data === "object" && data !== null && "name" in data) {
    console.log((data as { name: string }).name);
}

Use unknown anywhere data crosses a trust boundary: JSON.parse output, API responses before validation, anything coming from localStorage or URL parameters.

satisfies validates without widening the type

A common frustration with TypeScript config objects is that annotating them with a type loses the literal values. If you type a route config as Record<string, Route>, TypeScript forgets the exact paths — you just get string. The satisfies operator (added in TypeScript 4.9) solves this: it checks the value against a type without changing what TypeScript infers about it.

type Route = { path: string; title: string };

const ROUTES = {
    home:    { path: "/",        title: "Home"    },
    about:   { path: "/about",   title: "About"   },
    contact: { path: "/contact", title: "Contact" },
} satisfies Record<string, Route>;

// ROUTES.home.path is typed as "/" not just string

This is especially useful for large const maps where you want both type checking on the shape and access to the literal values downstream — route configs, feature flag maps, status code enums. If you're working with TypeScript or JavaScript code snippets outside your editor, the JavaScript Formatter can clean them up quickly.