typescript_error_handling 26 Q&As

TypeScript Error Handling FAQ & Answers

26 expert TypeScript Error Handling answers researched from official documentation. Every answer cites authoritative sources you can verify.

unknown

26 questions
A

Use type guards to safely narrow unknown errors to specific types. Pattern: function isError(error: unknown): error is Error { return error instanceof Error; } try { /* code / } catch (error) { if (isError(error)) { console.error(error.message); } else { console.error('Unknown error:', error); } }. Custom error guards: function isApiError(error: unknown): error is ApiError { return error instanceof ApiError || (typeof error === 'object' && error !== null && 'statusCode' in error); }. Multiple error types: if (error instanceof ValidationError) { / handle validation / } else if (error instanceof NetworkError) { / handle network / } else if (isError(error)) { / generic Error / } else { / unknown */ }. TypeScript 4.4+ uses unknown in catch blocks (safer than any). Best practices: (1) Create custom Error classes (class ApiError extends Error), (2) Use type predicates (error is Type), (3) Check instanceof before accessing properties, (4) Handle unknown case for non-Error throws. Avoid: type assertions without guards (as Error unsafe).

99% confidence
A

Log levels indicate severity and purpose. (1) debug: Detailed diagnostic info for development, disabled in production, example: 'Database query: SELECT * FROM users WHERE id=123', use for: variable values, function entry/exit, detailed flow. (2) info: General informational messages about normal operation, example: 'User logged in: [email protected]', 'Server started on port 3000', use for: important business events, successful operations, milestones. (3) warn: Potentially harmful situations that don't stop execution, example: 'API rate limit at 80%', 'Deprecated method called', use for: recoverable errors, performance issues, upcoming problems. (4) error: Error events that might allow app to continue, example: 'Failed to send email', 'Database connection timeout', use for: exceptions, failed operations, data corruption. Implementation: import { createLogger } from 'winston'; const logger = createLogger({ level: process.env.LOG_LEVEL || 'info' }); logger.debug('Debug'); logger.info('Info'); logger.warn('Warn'); logger.error('Error');. Production levels: error > warn > info, exclude debug. Use structured logging: logger.info('User login', { userId, timestamp }).

99% confidence
A

TypeScript 4.4+ types catch errors as unknown (requires useUnknownInCatchVariables: true or strict: true). Proper pattern: try { throw new Error('Failed'); } catch (error: unknown) { if (error instanceof Error) { console.error(error.message); console.error(error.stack); } else if (typeof error === 'string') { console.error(error); } else { console.error('Unknown error:', error); } }. Cannot annotate catch variable with specific type: catch (error: CustomError) throws TS1196 error. Reason: TypeScript cannot verify only CustomError is thrown. Helper function: function getErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; return String(error); }. Usage: catch (error) { const message = getErrorMessage(error); }. Custom error detection: if (error && typeof error === 'object' && 'statusCode' in error) { const apiError = error as ApiError; }. Best practice: always check error type before accessing properties, use type guards, avoid as assertions without validation.

99% confidence
A

instanceof Error checks if error object inherits from Error class in prototype chain. Returns true if: error.proto === Error.prototype or any ancestor. Works for: built-in Error, TypeError, ReferenceError, custom classes extending Error. Example: class ApiError extends Error { constructor(public statusCode: number, message: string) { super(message); this.name = 'ApiError'; } } const error = new ApiError(404, 'Not found'); error instanceof Error // true, error instanceof ApiError // true. Does NOT work for: plain objects ({ message: 'error' } instanceof Error // false), errors from different realms (iframe, worker - different Error constructors), serialized errors (JSON.parse'd errors lose prototype). Alternatives: check properties: error && typeof error === 'object' && 'message' in error && 'stack' in error. Use cases: (1) Type narrowing in catch blocks, (2) Differentiating error types, (3) Custom error handling. Combine with custom type guards for robust error checking: function isApiError(error: unknown): error is ApiError { return error instanceof Error && 'statusCode' in error; }.

99% confidence
A

Use utility function to normalize errors. Pattern: function toError(error: unknown): Error { if (error instanceof Error) return error; if (typeof error === 'string') return new Error(error); if (error && typeof error === 'object' && 'message' in error) { return new Error(String(error.message)); } return new Error('Unknown error: ' + String(error)); }. Usage: catch (error) { const err = toError(error); logger.error(err.message); Sentry.captureException(err); }. Preserve original error: function wrapError(error: unknown, context: string): Error { const normalized = toError(error); normalized.message = ${context}: ${normalized.message}; if (!(error instanceof Error)) { normalized.cause = error; // TypeScript 4.9+ Error.cause } return normalized; }. Use cause for chaining: try { await api.call(); } catch (error) { throw new Error('API call failed', { cause: toError(error) }); }. Advanced: preserve stack trace, custom properties: const err = toError(error); if ('statusCode' in error) { (err as any).statusCode = error.statusCode; }. Best practice: normalize at error boundary (global handler, API interceptor), not in business logic.

99% confidence
A

Async functions always return Promise. Syntax: async function fetchUser(id: string): Promise { const response = await fetch(/api/users/${id}); return response.json(); }. Return types: Promise for data, Promise for side effects only (logging, updates), Promise for success/failure checks. Implicit Promise wrapping: async function getValue(): Promise { return 42; } // returns Promise not number. Arrow functions: const fetchUser = async (id: string): Promise => { /* ... */ };. Error handling typed: async function save(): Promise { try { await api.save(); } catch (error: unknown) { if (error instanceof ApiError) throw error; throw new Error('Save failed'); } }. Void vs undefined: Promise means no meaningful return value, Promise explicit undefined return. Generic async: async function fetchData(url: string): Promise { return fetch(url).then(r => r.json()); }. Best practices: (1) Always specify return type explicitly, (2) Don't use Promise, (3) Handle errors with proper types, (4) Use generic types for reusable functions.

99% confidence
A

Promise returns no meaningful value, Promise returns success/failure flag. Use Promise for: side effects only, operations that complete without result. Example: async function sendEmail(to: string): Promise { await mailer.send(to); } // No return value needed. Use Promise for: success/failure indication, conditional logic. Example: async function isUserValid(id: string): Promise { const user = await db.findUser(id); return user !== null; }. Calling convention: Promise: await sendEmail('[email protected]'); // Fire and forget. Promise: if (await isUserValid(userId)) { /* proceed */ }. TypeScript quirk: function returning void can return any value (ignored), but async () => true not assignable to () => Promise (stricter). Event handlers: prefer Promise for onClick handlers, framework ignores return value. Error handling: both can throw errors. Best practice: (1) Use void for actions (save, delete, update), (2) Use boolean for checks (exists, valid, success), (3) Use specific types for data (Promise), (4) Document intent clearly in function names (checkUser vs getUser).

99% confidence
A

Type assertions override compiler inference (use sparingly). Syntax: const value = unknownValue as string; or const value = unknownValue; (JSX conflicts). Use cases: (1) DOM elements: const input = document.getElementById('email') as HTMLInputElement; input.value = '...'; (2) Type narrowing when compiler can't infer: const data = JSON.parse(jsonString) as User[]; (3) Third-party library types: const result = externalLib.getData() as MyType;. Dangers: bypasses type checking, can cause runtime errors if wrong. Example: const num = 'hello' as number; // Compiles, runtime error. Safe alternatives: (1) Type guards: if (typeof value === 'string') { /* value is string */ }, (2) Type annotations: const user: User = { ... }; catches errors at assignment, (3) Assertion functions: function assertIsString(value: unknown): asserts value is string { if (typeof value !== 'string') throw new Error('Not string'); }. Double assertion for incompatible types: const x = (value as unknown) as TargetType; // Anti-pattern. Best practice: avoid as assertions, use type guards, only assert when you have strong evidence, prefer satisfies operator (TypeScript 4.9+): const config = { ... } satisfies Config.

99% confidence
A

Use vue-tsc (official Vue 3 TypeScript checker) for type checking .vue files. Installation: npm install -D vue-tsc typescript. Add script to package.json: scripts: { type-check: vue-tsc --noEmit }. Run: npm run type-check (checks all .vue and .ts files, reports type errors without emitting JavaScript). Configuration in tsconfig.json: { compilerOptions: { strict: true, jsx: preserve, moduleResolution: bundler }, include: ['src//*.ts', 'src//.vue'], exclude: ['node_modules'] }. For watch mode (continuous checking during development): vue-tsc --noEmit --watch. IDE setup: Install Volar extension (replaces Vetur for Vue 3), enables real-time type checking in VS Code. Volar uses vue-tsc under the hood. Build integration: Run type checking before build: scripts: { prebuild: vue-tsc --noEmit, build: vite build } ensures type safety in CI/CD. Common errors: Cannot find module './Component.vue' - add volar/globals.d.ts with declare module '.vue'. Vue 3.3+ has better TypeScript support with defineProps generic syntax: const props = defineProps<{ msg: string }>(). Vite + vue-tsc = full type safety for Vue 3 SFC.

99% confidence
A

Use type guards to narrow unknown to specific types. Pattern: try { /* code */ } catch (error: unknown) { if (error instanceof Error) { console.error(error.message); } else if (typeof error === 'string') { console.error(error); } else { console.error('Unknown error:', String(error)); } }. Error TS18046 'error is of type unknown': error occurs when accessing properties without type checking. Fix: add type guard before access. Utility function: function isErrorWithMessage(error: unknown): error is { message: string } { return typeof error === 'object' && error !== null && 'message' in error && typeof (error as any).message === 'string'; } catch (error) { if (isErrorWithMessage(error)) { console.log(error.message); } }. Config option: useUnknownInCatchVariables: false reverts to any (not recommended). Why unknown is safer: forces explicit type checking, prevents unsafe property access, catches bugs at compile time. Best practices: (1) Keep useUnknownInCatchVariables: true (strict mode includes it), (2) Create reusable type guard functions, (3) Handle multiple error types explicitly, (4) Use assertion functions for complex validation.

99% confidence
A

JavaScript allows throwing any type, TypeScript uses unknown for safety. Possible throw types: Error objects (throw new Error('Failed')), strings (throw 'Error message'), numbers (throw 404), objects (throw { code: 404, message: 'Not found' }), null/undefined, anything. Handling pattern: catch (error: unknown) { if (error instanceof Error) { /* Standard Error / } else if (typeof error === 'string') { / String error / } else if (typeof error === 'object' && error !== null) { / Object with possible error info / } else { / Primitive or null */ } }. Best practice: always throw Error objects for stack traces. Create custom errors: class ValidationError extends Error { constructor(public field: string, message: string) { super(message); this.name = 'ValidationError'; } } throw new ValidationError('email', 'Invalid email');. Utility: function normalizeError(error: unknown): { message: string; stack?: string } { if (error instanceof Error) return { message: error.message, stack: error.stack }; if (typeof error === 'string') return { message: error }; return { message: String(error) }; }. TypeScript doesn't enforce throw types, document expected errors in JSDoc: @throws {ValidationError} When input invalid.

99% confidence
A

Use structured logging with proper levels and context. Setup with winston: import winston from 'winston'; const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.json(), transports: [new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.Console({ format: winston.format.simple() })] }). Log with context: logger.error('API call failed', { url, method, statusCode, userId, timestamp: new Date() });. Frontend logging: send to service: window.addEventListener('error', (event) => { fetch('/api/logs', { method: 'POST', body: JSON.stringify({ message: event.message, stack: event.error?.stack, url: window.location.href, userAgent: navigator.userAgent }) }); });. Use error monitoring: Sentry, LogRocket, Datadog for automatic tracking. Best practices: (1) Include context (user ID, request ID, timestamp), (2) Log error + stack trace, (3) Don't log sensitive data (passwords, tokens), (4) Use correlation IDs for distributed tracing, (5) Aggregate logs centrally (ELK stack, CloudWatch), (6) Set up alerts for critical errors, (7) Include environment info (version, browser).

99% confidence
A

Use the unknown type with runtime validation libraries like Zod or io-ts to validate and type API responses, rather than blindly asserting types with interfaces.

The Problem:
TypeScript provides static type checking only at compile time. When data comes from external API responses, TypeScript cannot guarantee the data matches the expected interface at runtime.

// UNSAFE: Type assertion without validation
interface User {
  id: number
  name: string
  email: string
}

const response = await fetch('/api/user')
const user = await response.json() as User // No runtime validation!
// If API returns different shape, runtime errors occur

Solution 1: Zod (Recommended for 2024)

Zod is a TypeScript-first schema validation library that validates data at runtime while automatically inferring TypeScript types.

Installation:

npm install zod

Basic Pattern:

import { z } from 'zod'

// Define schema with Zod
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional(),
  createdAt: z.string().datetime() // ISO 8601 date
})

// TypeScript type automatically inferred from schema
type User = z.infer<typeof UserSchema>

// Validate API response
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`)
  const data: unknown = await response.json()
  
  // Runtime validation - throws ZodError if invalid
  const user = UserSchema.parse(data)
  return user // Type-safe User object
}

Safe Parsing (Non-Throwing):

const result = UserSchema.safeParse(data)

if (result.success) {
  const user = result.data // User type
  console.log(user.name)
} else {
  console.error('Validation failed:', result.error.issues)
  // result.error.issues contains detailed validation errors
}

Array Responses:

const UsersArraySchema = z.array(UserSchema)

async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users')
  const data: unknown = await response.json()
  return UsersArraySchema.parse(data)
}

Nested/Complex Objects:

const ApiResponseSchema = z.object({
  success: z.boolean(),
  data: z.object({
    users: z.array(UserSchema),
    pagination: z.object({
      total: z.number(),
      page: z.number(),
      perPage: z.number()
    })
  }),
  message: z.string().optional()
})

type ApiResponse = z.infer<typeof ApiResponseSchema>

Transformations:

const UserSchema = z.object({
  id: z.number(),
  createdAt: z.string().transform((val) => new Date(val))
})

// Inferred type: { id: number, createdAt: Date }

Default Values:

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  role: z.string().default('user')
})

const user = UserSchema.parse({ id: 1, name: 'John' })
// user.role === 'user'

Solution 2: io-ts (Alternative)

io-ts is another runtime validation library with TypeScript integration.

import * as t from 'io-ts'
import { isRight } from 'fp-ts/Either'

const UserCodec = t.type({
  id: t.number,
  name: t.string,
  email: t.string
})

type User = t.TypeOf<typeof UserCodec>

const result = UserCodec.decode(unknownData)

if (isRight(result)) {
  const user = result.right // User type
} else {
  console.error('Validation failed')
}

Solution 3: Type Guards (Manual Validation)

For simple cases without dependencies:

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

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    typeof obj.id === 'number' &&
    'name' in obj &&
    typeof obj.name === 'string' &&
    'email' in obj &&
    typeof obj.email === 'string'
  )
}

const data: unknown = await response.json()

if (isUser(data)) {
  // data is User type
  console.log(data.name)
} else {
  throw new Error('Invalid user data')
}

Production Pattern (API Wrapper):

import { z } from 'zod'

class ApiClient {
  async fetch<T>(url: string, schema: z.ZodSchema<T>): Promise<T> {
    try {
      const response = await fetch(url)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      const data: unknown = await response.json()
      return schema.parse(data)
    } catch (error) {
      if (error instanceof z.ZodError) {
        console.error('Validation failed:', error.issues)
        throw new Error('Invalid API response format')
      }
      throw error
    }
  }
}

// Usage
const api = new ApiClient()
const user = await api.fetch('/api/users/1', UserSchema)

Form Validation with Zod:

const LoginFormSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters')
})

type LoginForm = z.infer<typeof LoginFormSchema>

function handleSubmit(formData: unknown) {
  const result = LoginFormSchema.safeParse(formData)
  
  if (!result.success) {
    // Show validation errors to user
    result.error.issues.forEach(issue => {
      console.log(`${issue.path}: ${issue.message}`)
    })
    return
  }
  
  // result.data is typed as LoginForm
  submitLogin(result.data)
}

Zod vs io-ts Comparison (2024):

Feature Zod io-ts
TypeScript Integration Excellent (TypeScript-first) Good (requires fp-ts)
Bundle Size ~12KB minified ~30KB with fp-ts
API Simplicity Very simple More complex (FP concepts)
Dependencies Zero Requires fp-ts
Documentation Extensive Good but functional-heavy
Adoption (2024) Very high Moderate
Type Inference Automatic Manual (TypeOf)

Best Practices:

  1. Always use unknown for API responses, never any or direct type assertions
  2. Validate at API boundaries (where data enters your application)
  3. Use Zod for new projects (2024 standard, tested with TypeScript 5.5+)
  4. Define schemas close to usage for maintainability
  5. Use safeParse for user-facing errors (non-throwing validation)
  6. Use parse for internal APIs (throw on invalid data)
  7. Create reusable schemas for common data structures
  8. Add custom error messages for better UX
  9. Transform data during validation (dates, enums, normalizations)
  10. Never skip validation for external data sources

When to Use Each Approach:

  • Zod: Default choice for 2024, best DX, broad adoption, zero dependencies
  • io-ts: Functional programming projects, teams already using fp-ts
  • Type Guards: Simple cases, no external dependencies allowed, minimal validation needs

Error Handling Example:

import { z } from 'zod'

const UserSchema = z.object({ id: z.number(), name: z.string() })

async function fetchUser(id: number) {
  try {
    const response = await fetch(`/api/users/${id}`)
    const data: unknown = await response.json()
    
    const user = UserSchema.parse(data)
    return { success: true, data: user }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        success: false,
        error: 'Invalid response format',
        details: error.issues
      }
    }
    
    return {
      success: false,
      error: 'Network error'
    }
  }
}
99% confidence
A

Use type guards to safely narrow unknown errors to specific types. Pattern: function isError(error: unknown): error is Error { return error instanceof Error; } try { /* code / } catch (error) { if (isError(error)) { console.error(error.message); } else { console.error('Unknown error:', error); } }. Custom error guards: function isApiError(error: unknown): error is ApiError { return error instanceof ApiError || (typeof error === 'object' && error !== null && 'statusCode' in error); }. Multiple error types: if (error instanceof ValidationError) { / handle validation / } else if (error instanceof NetworkError) { / handle network / } else if (isError(error)) { / generic Error / } else { / unknown */ }. TypeScript 4.4+ uses unknown in catch blocks (safer than any). Best practices: (1) Create custom Error classes (class ApiError extends Error), (2) Use type predicates (error is Type), (3) Check instanceof before accessing properties, (4) Handle unknown case for non-Error throws. Avoid: type assertions without guards (as Error unsafe).

99% confidence
A

Log levels indicate severity and purpose. (1) debug: Detailed diagnostic info for development, disabled in production, example: 'Database query: SELECT * FROM users WHERE id=123', use for: variable values, function entry/exit, detailed flow. (2) info: General informational messages about normal operation, example: 'User logged in: [email protected]', 'Server started on port 3000', use for: important business events, successful operations, milestones. (3) warn: Potentially harmful situations that don't stop execution, example: 'API rate limit at 80%', 'Deprecated method called', use for: recoverable errors, performance issues, upcoming problems. (4) error: Error events that might allow app to continue, example: 'Failed to send email', 'Database connection timeout', use for: exceptions, failed operations, data corruption. Implementation: import { createLogger } from 'winston'; const logger = createLogger({ level: process.env.LOG_LEVEL || 'info' }); logger.debug('Debug'); logger.info('Info'); logger.warn('Warn'); logger.error('Error');. Production levels: error > warn > info, exclude debug. Use structured logging: logger.info('User login', { userId, timestamp }).

99% confidence
A

TypeScript 4.4+ types catch errors as unknown (requires useUnknownInCatchVariables: true or strict: true). Proper pattern: try { throw new Error('Failed'); } catch (error: unknown) { if (error instanceof Error) { console.error(error.message); console.error(error.stack); } else if (typeof error === 'string') { console.error(error); } else { console.error('Unknown error:', error); } }. Cannot annotate catch variable with specific type: catch (error: CustomError) throws TS1196 error. Reason: TypeScript cannot verify only CustomError is thrown. Helper function: function getErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; return String(error); }. Usage: catch (error) { const message = getErrorMessage(error); }. Custom error detection: if (error && typeof error === 'object' && 'statusCode' in error) { const apiError = error as ApiError; }. Best practice: always check error type before accessing properties, use type guards, avoid as assertions without validation.

99% confidence
A

instanceof Error checks if error object inherits from Error class in prototype chain. Returns true if: error.proto === Error.prototype or any ancestor. Works for: built-in Error, TypeError, ReferenceError, custom classes extending Error. Example: class ApiError extends Error { constructor(public statusCode: number, message: string) { super(message); this.name = 'ApiError'; } } const error = new ApiError(404, 'Not found'); error instanceof Error // true, error instanceof ApiError // true. Does NOT work for: plain objects ({ message: 'error' } instanceof Error // false), errors from different realms (iframe, worker - different Error constructors), serialized errors (JSON.parse'd errors lose prototype). Alternatives: check properties: error && typeof error === 'object' && 'message' in error && 'stack' in error. Use cases: (1) Type narrowing in catch blocks, (2) Differentiating error types, (3) Custom error handling. Combine with custom type guards for robust error checking: function isApiError(error: unknown): error is ApiError { return error instanceof Error && 'statusCode' in error; }.

99% confidence
A

Use utility function to normalize errors. Pattern: function toError(error: unknown): Error { if (error instanceof Error) return error; if (typeof error === 'string') return new Error(error); if (error && typeof error === 'object' && 'message' in error) { return new Error(String(error.message)); } return new Error('Unknown error: ' + String(error)); }. Usage: catch (error) { const err = toError(error); logger.error(err.message); Sentry.captureException(err); }. Preserve original error: function wrapError(error: unknown, context: string): Error { const normalized = toError(error); normalized.message = ${context}: ${normalized.message}; if (!(error instanceof Error)) { normalized.cause = error; // TypeScript 4.9+ Error.cause } return normalized; }. Use cause for chaining: try { await api.call(); } catch (error) { throw new Error('API call failed', { cause: toError(error) }); }. Advanced: preserve stack trace, custom properties: const err = toError(error); if ('statusCode' in error) { (err as any).statusCode = error.statusCode; }. Best practice: normalize at error boundary (global handler, API interceptor), not in business logic.

99% confidence
A

Async functions always return Promise. Syntax: async function fetchUser(id: string): Promise { const response = await fetch(/api/users/${id}); return response.json(); }. Return types: Promise for data, Promise for side effects only (logging, updates), Promise for success/failure checks. Implicit Promise wrapping: async function getValue(): Promise { return 42; } // returns Promise not number. Arrow functions: const fetchUser = async (id: string): Promise => { /* ... */ };. Error handling typed: async function save(): Promise { try { await api.save(); } catch (error: unknown) { if (error instanceof ApiError) throw error; throw new Error('Save failed'); } }. Void vs undefined: Promise means no meaningful return value, Promise explicit undefined return. Generic async: async function fetchData(url: string): Promise { return fetch(url).then(r => r.json()); }. Best practices: (1) Always specify return type explicitly, (2) Don't use Promise, (3) Handle errors with proper types, (4) Use generic types for reusable functions.

99% confidence
A

Promise returns no meaningful value, Promise returns success/failure flag. Use Promise for: side effects only, operations that complete without result. Example: async function sendEmail(to: string): Promise { await mailer.send(to); } // No return value needed. Use Promise for: success/failure indication, conditional logic. Example: async function isUserValid(id: string): Promise { const user = await db.findUser(id); return user !== null; }. Calling convention: Promise: await sendEmail('[email protected]'); // Fire and forget. Promise: if (await isUserValid(userId)) { /* proceed */ }. TypeScript quirk: function returning void can return any value (ignored), but async () => true not assignable to () => Promise (stricter). Event handlers: prefer Promise for onClick handlers, framework ignores return value. Error handling: both can throw errors. Best practice: (1) Use void for actions (save, delete, update), (2) Use boolean for checks (exists, valid, success), (3) Use specific types for data (Promise), (4) Document intent clearly in function names (checkUser vs getUser).

99% confidence
A

Type assertions override compiler inference (use sparingly). Syntax: const value = unknownValue as string; or const value = unknownValue; (JSX conflicts). Use cases: (1) DOM elements: const input = document.getElementById('email') as HTMLInputElement; input.value = '...'; (2) Type narrowing when compiler can't infer: const data = JSON.parse(jsonString) as User[]; (3) Third-party library types: const result = externalLib.getData() as MyType;. Dangers: bypasses type checking, can cause runtime errors if wrong. Example: const num = 'hello' as number; // Compiles, runtime error. Safe alternatives: (1) Type guards: if (typeof value === 'string') { /* value is string */ }, (2) Type annotations: const user: User = { ... }; catches errors at assignment, (3) Assertion functions: function assertIsString(value: unknown): asserts value is string { if (typeof value !== 'string') throw new Error('Not string'); }. Double assertion for incompatible types: const x = (value as unknown) as TargetType; // Anti-pattern. Best practice: avoid as assertions, use type guards, only assert when you have strong evidence, prefer satisfies operator (TypeScript 4.9+): const config = { ... } satisfies Config.

99% confidence
A

Use vue-tsc (official Vue 3 TypeScript checker) for type checking .vue files. Installation: npm install -D vue-tsc typescript. Add script to package.json: scripts: { type-check: vue-tsc --noEmit }. Run: npm run type-check (checks all .vue and .ts files, reports type errors without emitting JavaScript). Configuration in tsconfig.json: { compilerOptions: { strict: true, jsx: preserve, moduleResolution: bundler }, include: ['src//*.ts', 'src//.vue'], exclude: ['node_modules'] }. For watch mode (continuous checking during development): vue-tsc --noEmit --watch. IDE setup: Install Volar extension (replaces Vetur for Vue 3), enables real-time type checking in VS Code. Volar uses vue-tsc under the hood. Build integration: Run type checking before build: scripts: { prebuild: vue-tsc --noEmit, build: vite build } ensures type safety in CI/CD. Common errors: Cannot find module './Component.vue' - add volar/globals.d.ts with declare module '.vue'. Vue 3.3+ has better TypeScript support with defineProps generic syntax: const props = defineProps<{ msg: string }>(). Vite + vue-tsc = full type safety for Vue 3 SFC.

99% confidence
A

Use type guards to narrow unknown to specific types. Pattern: try { /* code */ } catch (error: unknown) { if (error instanceof Error) { console.error(error.message); } else if (typeof error === 'string') { console.error(error); } else { console.error('Unknown error:', String(error)); } }. Error TS18046 'error is of type unknown': error occurs when accessing properties without type checking. Fix: add type guard before access. Utility function: function isErrorWithMessage(error: unknown): error is { message: string } { return typeof error === 'object' && error !== null && 'message' in error && typeof (error as any).message === 'string'; } catch (error) { if (isErrorWithMessage(error)) { console.log(error.message); } }. Config option: useUnknownInCatchVariables: false reverts to any (not recommended). Why unknown is safer: forces explicit type checking, prevents unsafe property access, catches bugs at compile time. Best practices: (1) Keep useUnknownInCatchVariables: true (strict mode includes it), (2) Create reusable type guard functions, (3) Handle multiple error types explicitly, (4) Use assertion functions for complex validation.

99% confidence
A

JavaScript allows throwing any type, TypeScript uses unknown for safety. Possible throw types: Error objects (throw new Error('Failed')), strings (throw 'Error message'), numbers (throw 404), objects (throw { code: 404, message: 'Not found' }), null/undefined, anything. Handling pattern: catch (error: unknown) { if (error instanceof Error) { /* Standard Error / } else if (typeof error === 'string') { / String error / } else if (typeof error === 'object' && error !== null) { / Object with possible error info / } else { / Primitive or null */ } }. Best practice: always throw Error objects for stack traces. Create custom errors: class ValidationError extends Error { constructor(public field: string, message: string) { super(message); this.name = 'ValidationError'; } } throw new ValidationError('email', 'Invalid email');. Utility: function normalizeError(error: unknown): { message: string; stack?: string } { if (error instanceof Error) return { message: error.message, stack: error.stack }; if (typeof error === 'string') return { message: error }; return { message: String(error) }; }. TypeScript doesn't enforce throw types, document expected errors in JSDoc: @throws {ValidationError} When input invalid.

99% confidence
A

Use structured logging with proper levels and context. Setup with winston: import winston from 'winston'; const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.json(), transports: [new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.Console({ format: winston.format.simple() })] }). Log with context: logger.error('API call failed', { url, method, statusCode, userId, timestamp: new Date() });. Frontend logging: send to service: window.addEventListener('error', (event) => { fetch('/api/logs', { method: 'POST', body: JSON.stringify({ message: event.message, stack: event.error?.stack, url: window.location.href, userAgent: navigator.userAgent }) }); });. Use error monitoring: Sentry, LogRocket, Datadog for automatic tracking. Best practices: (1) Include context (user ID, request ID, timestamp), (2) Log error + stack trace, (3) Don't log sensitive data (passwords, tokens), (4) Use correlation IDs for distributed tracing, (5) Aggregate logs centrally (ELK stack, CloudWatch), (6) Set up alerts for critical errors, (7) Include environment info (version, browser).

99% confidence
A

Use the unknown type with runtime validation libraries like Zod or io-ts to validate and type API responses, rather than blindly asserting types with interfaces.

The Problem:
TypeScript provides static type checking only at compile time. When data comes from external API responses, TypeScript cannot guarantee the data matches the expected interface at runtime.

// UNSAFE: Type assertion without validation
interface User {
  id: number
  name: string
  email: string
}

const response = await fetch('/api/user')
const user = await response.json() as User // No runtime validation!
// If API returns different shape, runtime errors occur

Solution 1: Zod (Recommended for 2024)

Zod is a TypeScript-first schema validation library that validates data at runtime while automatically inferring TypeScript types.

Installation:

npm install zod

Basic Pattern:

import { z } from 'zod'

// Define schema with Zod
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional(),
  createdAt: z.string().datetime() // ISO 8601 date
})

// TypeScript type automatically inferred from schema
type User = z.infer<typeof UserSchema>

// Validate API response
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`)
  const data: unknown = await response.json()
  
  // Runtime validation - throws ZodError if invalid
  const user = UserSchema.parse(data)
  return user // Type-safe User object
}

Safe Parsing (Non-Throwing):

const result = UserSchema.safeParse(data)

if (result.success) {
  const user = result.data // User type
  console.log(user.name)
} else {
  console.error('Validation failed:', result.error.issues)
  // result.error.issues contains detailed validation errors
}

Array Responses:

const UsersArraySchema = z.array(UserSchema)

async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users')
  const data: unknown = await response.json()
  return UsersArraySchema.parse(data)
}

Nested/Complex Objects:

const ApiResponseSchema = z.object({
  success: z.boolean(),
  data: z.object({
    users: z.array(UserSchema),
    pagination: z.object({
      total: z.number(),
      page: z.number(),
      perPage: z.number()
    })
  }),
  message: z.string().optional()
})

type ApiResponse = z.infer<typeof ApiResponseSchema>

Transformations:

const UserSchema = z.object({
  id: z.number(),
  createdAt: z.string().transform((val) => new Date(val))
})

// Inferred type: { id: number, createdAt: Date }

Default Values:

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  role: z.string().default('user')
})

const user = UserSchema.parse({ id: 1, name: 'John' })
// user.role === 'user'

Solution 2: io-ts (Alternative)

io-ts is another runtime validation library with TypeScript integration.

import * as t from 'io-ts'
import { isRight } from 'fp-ts/Either'

const UserCodec = t.type({
  id: t.number,
  name: t.string,
  email: t.string
})

type User = t.TypeOf<typeof UserCodec>

const result = UserCodec.decode(unknownData)

if (isRight(result)) {
  const user = result.right // User type
} else {
  console.error('Validation failed')
}

Solution 3: Type Guards (Manual Validation)

For simple cases without dependencies:

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

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    typeof obj.id === 'number' &&
    'name' in obj &&
    typeof obj.name === 'string' &&
    'email' in obj &&
    typeof obj.email === 'string'
  )
}

const data: unknown = await response.json()

if (isUser(data)) {
  // data is User type
  console.log(data.name)
} else {
  throw new Error('Invalid user data')
}

Production Pattern (API Wrapper):

import { z } from 'zod'

class ApiClient {
  async fetch<T>(url: string, schema: z.ZodSchema<T>): Promise<T> {
    try {
      const response = await fetch(url)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      const data: unknown = await response.json()
      return schema.parse(data)
    } catch (error) {
      if (error instanceof z.ZodError) {
        console.error('Validation failed:', error.issues)
        throw new Error('Invalid API response format')
      }
      throw error
    }
  }
}

// Usage
const api = new ApiClient()
const user = await api.fetch('/api/users/1', UserSchema)

Form Validation with Zod:

const LoginFormSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters')
})

type LoginForm = z.infer<typeof LoginFormSchema>

function handleSubmit(formData: unknown) {
  const result = LoginFormSchema.safeParse(formData)
  
  if (!result.success) {
    // Show validation errors to user
    result.error.issues.forEach(issue => {
      console.log(`${issue.path}: ${issue.message}`)
    })
    return
  }
  
  // result.data is typed as LoginForm
  submitLogin(result.data)
}

Zod vs io-ts Comparison (2024):

Feature Zod io-ts
TypeScript Integration Excellent (TypeScript-first) Good (requires fp-ts)
Bundle Size ~12KB minified ~30KB with fp-ts
API Simplicity Very simple More complex (FP concepts)
Dependencies Zero Requires fp-ts
Documentation Extensive Good but functional-heavy
Adoption (2024) Very high Moderate
Type Inference Automatic Manual (TypeOf)

Best Practices:

  1. Always use unknown for API responses, never any or direct type assertions
  2. Validate at API boundaries (where data enters your application)
  3. Use Zod for new projects (2024 standard, tested with TypeScript 5.5+)
  4. Define schemas close to usage for maintainability
  5. Use safeParse for user-facing errors (non-throwing validation)
  6. Use parse for internal APIs (throw on invalid data)
  7. Create reusable schemas for common data structures
  8. Add custom error messages for better UX
  9. Transform data during validation (dates, enums, normalizations)
  10. Never skip validation for external data sources

When to Use Each Approach:

  • Zod: Default choice for 2024, best DX, broad adoption, zero dependencies
  • io-ts: Functional programming projects, teams already using fp-ts
  • Type Guards: Simple cases, no external dependencies allowed, minimal validation needs

Error Handling Example:

import { z } from 'zod'

const UserSchema = z.object({ id: z.number(), name: z.string() })

async function fetchUser(id: number) {
  try {
    const response = await fetch(`/api/users/${id}`)
    const data: unknown = await response.json()
    
    const user = UserSchema.parse(data)
    return { success: true, data: user }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        success: false,
        error: 'Invalid response format',
        details: error.issues
      }
    }
    
    return {
      success: false,
      error: 'Network error'
    }
  }
}
99% confidence