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 undefinedundefined is not a functionerrors in production- Silent
NaNpropagation 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 fieldsIncremental 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 runtimeRoot Cause
- API responses default to
anywithout explicit typing - Third-party libraries without type definitions
- Developers using
anyto "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 user5. 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-nameCreate 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
| Practice | Tool/Feature |
|---|---|
| Enable strict mode | tsconfig.json — "strict": true |
Ban any usage | ESLint @typescript-eslint/no-explicit-any |
| Validate external data | Zod, io-ts, Valibot |
| Type API responses | Codegen from OpenAPI/GraphQL schemas |
| Check exhaustiveness | never type in switch defaults |
| Prevent floating promises | @typescript-eslint/no-floating-promises |
| Typed event handlers | React type definitions (React.ChangeEvent, etc.) |
| Third-party types | @types/* packages or custom .d.ts files |