Steven's Knowledge
Troubleshooting

Type Safety

Type-related problems in frontend development and TypeScript-based solutions

Type Safety Troubleshooting

Type-related issues are among the most common sources of runtime errors in frontend applications. This guide covers typical type problems and their solutions using TypeScript and related tooling.


1. No Type Definitions — Runtime Type Errors

Problem

Plain JavaScript provides no compile-time type checking. Common symptoms:

// No warning until runtime
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

calculateTotal("not an array"); // TypeError: items.reduce is not a function
calculateTotal([{ cost: 10 }]); // NaN — silent failure, `price` is undefined
  • undefined is not a function errors in production
  • Silent NaN propagation through calculations
  • Incorrect data shapes passed between components

Root Cause

JavaScript is dynamically typed — variables can hold any value at any time, and type mismatches are only caught at runtime (if at all).

Solution: Adopt TypeScript with Strict Mode

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,                    // Enable all strict checks
    "noUncheckedIndexedAccess": true,  // Arrays/objects may return undefined
    "exactOptionalPropertyTypes": true, // Strict optional property handling
    "noImplicitOverride": true,
    "forceConsistentCasingInFileNames": true
  }
}
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

calculateTotal("not an array"); // ✗ Compile error
calculateTotal([{ cost: 10 }]); // ✗ Compile error — missing required fields

Incremental Migration Strategy

For existing JavaScript projects:

// tsconfig.json — gradual adoption
{
  "compilerOptions": {
    "allowJs": true,           // Allow JS files alongside TS
    "checkJs": false,          // Don't check JS files initially
    "strict": false,           // Start lenient
    "noImplicitAny": false     // Allow implicit any during migration
  },
  "include": ["src/**/*"]
}

Migrate file-by-file: .js.ts, enabling stricter checks progressively.


2. Unsafe any Type Propagation

Problem

Overuse of any defeats TypeScript's purpose — errors silently propagate:

function fetchUser(): any {
  return fetch('/api/user').then(res => res.json());
}

const user = fetchUser();
console.log(user.nmae); // No error — typo goes undetected
user.toFixed(2);        // No error — might crash at runtime

Root Cause

  • API responses default to any without explicit typing
  • Third-party libraries without type definitions
  • Developers using any to "fix" type errors quickly

Solution: Eliminate any Systematically

Use unknown instead of any:

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

const data = parseJSON('{"name": "Alice"}');
// data.name;  // ✗ Error — must narrow type first

// Type narrowing with type guards
if (isUser(data)) {
  console.log(data.name); // ✓ Safe access
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    typeof (value as Record<string, unknown>).name === 'string'
  );
}

Runtime validation with Zod:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'viewer']),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return UserSchema.parse(data); // Runtime validation + full type safety
}

ESLint rules to prevent any:

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-unsafe-assignment": "error",
    "@typescript-eslint/no-unsafe-member-access": "error",
    "@typescript-eslint/no-unsafe-call": "error",
    "@typescript-eslint/no-unsafe-return": "error"
  }
}

3. Incorrect Union / Discriminated Union Handling

Problem

type Result = { status: 'success'; data: User } | { status: 'error'; message: string };

function handleResult(result: Result) {
  console.log(result.data);    // ✗ Error — `data` doesn't exist on error variant
  console.log(result.message); // ✗ Error — `message` doesn't exist on success variant
}

Solution: Exhaustive Pattern Matching

function handleResult(result: Result): string {
  switch (result.status) {
    case 'success':
      return `User: ${result.data.name}`;
    case 'error':
      return `Error: ${result.message}`;
    default:
      // Exhaustiveness check — compile error if a case is missed
      const _exhaustive: never = result;
      return _exhaustive;
  }
}

4. Generic Type Constraints Missing

Problem

function getProperty<T>(obj: T, key: string) {
  return obj[key]; // ✗ No guarantee `key` exists on `obj`
}

Solution: Constrain Generics with keyof

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]; // ✓ Type-safe property access
}

const user = { name: 'Alice', age: 30 };
getProperty(user, 'name');  // ✓ Returns string
getProperty(user, 'email'); // ✗ Compile error — 'email' is not a key of user

5. Incorrect Type Narrowing with typeof / instanceof

Problem

function process(value: string | number | null) {
  if (typeof value === 'object') {
    // value is `null` here, not an object! typeof null === 'object'
    value.toString(); // Runtime error
  }
}

Solution: Explicit Null Checks

function process(value: string | number | null) {
  if (value === null) {
    return 'null value';
  }
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  return value.toFixed(2);
}

6. Third-Party Library Type Mismatches

Problem

  • Library has no @types/* package
  • Type definitions are outdated or incorrect
  • Breaking changes between library version and type version

Solution

Check for existing types:

# Search for type definitions
npm search @types/library-name

# Install types
npm install -D @types/library-name

Create module declarations for untyped libraries:

// src/types/untyped-lib.d.ts
declare module 'untyped-lib' {
  export interface Config {
    option1: string;
    option2?: number;
  }
  export function initialize(config: Config): void;
  export default function main(): Promise<void>;
}

Use skipLibCheck for problematic type packages:

{
  "compilerOptions": {
    "skipLibCheck": true  // Skip type checking of .d.ts files
  }
}

7. Async/Await Type Handling

Problem

// Forgetting to await — gets Promise<T> instead of T
async function getUser() {
  const user = fetchUser(); // Missing await — user is Promise<User>, not User
  console.log(user.name);   // undefined — accessing property on Promise
}

Solution: Enable @typescript-eslint/no-floating-promises

{
  "rules": {
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/await-thenable": "error",
    "@typescript-eslint/no-misused-promises": "error"
  }
}
async function getUser(): Promise<User> {
  const user = await fetchUser(); // ✓ Properly awaited
  console.log(user.name);
  return user;
}

8. Event Handler Type Issues in React

Problem

// What type is `e`?
function handleChange(e) {  // Implicit any
  console.log(e.target.value);
}

// Wrong event type
function handleClick(e: React.ChangeEvent<HTMLInputElement>) {
  e.preventDefault(); // This works, but the type is wrong for a click handler
}

Solution: Correct React Event Types

// Form events
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const value = e.target.value;
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
};

// Mouse events
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e.clientX, e.clientY);
};

// Keyboard events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter') submit();
};

// Drag events
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
  const files = e.dataTransfer.files;
};

Summary: Type Safety Checklist

PracticeTool/Feature
Enable strict modetsconfig.json"strict": true
Ban any usageESLint @typescript-eslint/no-explicit-any
Validate external dataZod, io-ts, Valibot
Type API responsesCodegen from OpenAPI/GraphQL schemas
Check exhaustivenessnever type in switch defaults
Prevent floating promises@typescript-eslint/no-floating-promises
Typed event handlersReact type definitions (React.ChangeEvent, etc.)
Third-party types@types/* packages or custom .d.ts files

On this page