TypeScript Tips Every JavaScript Developer Should Know
TypeScriptJavaScriptTips

TypeScript Tips Every JavaScript Developer Should Know

Use Union Types Instead of Enums

TypeScript enums generate JavaScript code at runtime — they are one of the few TypeScript constructs that do. They can also cause unexpected issues with tree shaking and const assertion. The modern alternative is a union of string literals, which compiles to zero JavaScript:

// Avoid
enum Direction { Up, Down, Left, Right }

// Prefer
type Direction = 'Up' | 'Down' | 'Left' | 'Right';
const move = (dir: Direction) => { /* ... */ };
move('Up');  // type-safe, zero runtime overhead

If you need the enum-like ability to iterate over values, use as const with an array:

const DIRECTIONS = ['Up', 'Down', 'Left', 'Right'] as const;
type Direction = typeof DIRECTIONS[number];
// → 'Up' | 'Down' | 'Left' | 'Right'

Discriminated Unions for State Machines

Discriminated unions are TypeScript's most powerful feature for modeling application state. When every variant has a shared literal property ("discriminant"), TypeScript can narrow the type perfectly inside a switch or if statement:

type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string; retryable: boolean };

function render<T>(state: RequestState<T>) {
  switch (state.status) {
    case 'loading': return <Spinner />;
    case 'success': return <DataView data={state.data} />;  // data is T here
    case 'error': return <ErrorBanner message={state.message} />;  // message is string
    case 'idle': return null;
  }
}

TypeScript will tell you if you forget to handle a case. This pattern eliminates entire categories of runtime errors — the "cannot read property 'data' of undefined" class of bugs.

Master the Built-In Utility Types

TypeScript ships with utility types that transform other types. Knowing them prevents you from writing complex mapped types from scratch:

  • Partial<T> — makes all properties optional. Useful for update operations.
  • Required<T> — makes all properties required. The reverse of Partial.
  • Readonly<T> — makes all properties read-only.
  • Pick<T, K> — keeps only the specified keys: Pick<User, 'id' | 'email'>
  • Omit<T, K> — removes the specified keys: Omit<User, 'password'>
  • Record<K, V> — creates an object type with keys K and values V.
  • ReturnType<T> — extracts the return type of a function type.
  • Awaited<T> — unwraps a Promise type: Awaited<Promise<string>>string
  • Parameters<T> — extracts parameter types as a tuple.
  • NonNullable<T> — removes null and undefined from a type.

Use as const for Literal Inference

Without as const, TypeScript infers the widest possible type. With it, TypeScript infers the narrowest literal type:

const config = { port: 8080, env: 'production' };
// type: { port: number; env: string }  ← widened

const config = { port: 8080, env: 'production' } as const;
// type: { readonly port: 8080; readonly env: "production" }  ← literal

This is especially powerful for defining configuration objects that feed into union types, and for route or status code definitions.

Template Literal Types

TypeScript can do string manipulation at the type level:

type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// → 'onClick' | 'onFocus' | 'onBlur'

type CSSProperty = 'margin' | 'padding';
type CSSDirection = 'top' | 'bottom' | 'left' | 'right';
type CSSLonghand = `${CSSProperty}-${CSSDirection}`;
// → 'margin-top' | 'margin-bottom' | 'padding-left' | ...

Conditional Types

Conditional types allow type-level logic using the ternary operator pattern:

type IsArray<T> = T extends any[] ? true : false;
type ElementType<T> = T extends (infer E)[] ? E : never;

type A = ElementType<string[]>;   // string
type B = ElementType<number[]>;   // number
type C = ElementType<string>;     // never

The satisfies Operator

Added in TypeScript 4.9, satisfies validates that a value matches a type without widening it to that type. You get type checking without losing the specific literal types:

type Config = Record<string, string | number>;

const config = {
  port: 8080,
  host: 'localhost',
} satisfies Config;

// config.port is still number (not string | number)
// config.host is still string (not string | number)
config.port.toFixed(2);   // ✓ works — TypeScript knows it's number

Strict Mode Is Non-Negotiable

Always enable "strict": true in tsconfig.json. This enables: strictNullChecks, noImplicitAny, strictFunctionTypes, and several others. Starting a project without strict mode means accumulating technical debt that becomes painful to fix later. The extra type errors you get initially are bugs you are catching before runtime.

Generate Interfaces from API Responses

Use PureFormatter's JSON to TypeScript converter to instantly generate TypeScript interfaces from any JSON response. Paste the API response, get a complete interface definition — then customize it with more specific types like discriminated unions and utility types where appropriate.

Fredy
Written by
Fredy
Senior Developer & Technical Writer

Fredy is a full-stack developer with 8+ years of experience building web applications. He writes about developer tools, best practices, and the craft of clean code.