Logo

Home

About

Blog

Contact

Guestbook

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Type Assertions and Type Casting

Table of contents

  • Share on X
  • Discuss on X

Related Articles

The any, unknown, and never Types
TypeScript
8m
Dec 8, 2025

The any, unknown, and never Types

TypeScript has three special types that handle edge cases: any (the escape hatch), unknown (the type-safe alternative), and never (for impossible values). Understanding when and how to use these types is crucial for writing safe yet flexible TypeScript code. This guide covers the differences between these types, shows you how to work with unknown safely, and demonstrates how never enables exhaustive type checking and prevents runtime errors. Learn to use these types effectively while maintaining type safety.

#TypeScript#Type System+5
Working with Arrays and Tuples
TypeScript
7m
Nov 28, 2025

Working with Arrays and Tuples

Arrays and tuples are essential for working with collections in TypeScript. While arrays hold multiple values of the same type, tuples allow you to work with fixed-length arrays where each position has a specific type. This guide covers array types, readonly patterns, tuple best practices, and practical examples that you'll use in real applications. Learn how to leverage TypeScript's type system for safer, more maintainable collection handling.

#TypeScript#Arrays+5
Basic Types in TypeScript
TypeScript
9m
Nov 23, 2025

Basic Types in TypeScript

TypeScript's type system starts with fundamental building blocks: primitive types like string, number, and boolean, along with arrays and type annotations. Understanding these basic types is essential for writing type-safe code. This comprehensive guide walks you through each primitive type, shows you how to use type annotations effectively, and teaches you best practices for working with arrays and basic data structures in TypeScript.

#TypeScript#Types+5

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2025 • All rights reserved

Contents

  • What Are Type Assertions?
  • The as Syntax
    • Basic as Syntax
    • Why as is Preferred
  • Angle-Bracket Syntax
  • Assertions vs Type Casting
  • When to Use Type Assertions
    • DOM Manipulation
    • Working with unknown
    • API Responses
    • Type Narrowing
  • Double Assertions
  • Const Assertions
  • Non-Null Assertions
  • Practical Examples
    • Form Handling
    • Event Handling
    • Local Storage
    • Third-Party Libraries
  • Dangers of Type Assertions
  • Best Practices
  • Common Mistakes
  • Next Steps
TypeScript

Type Assertions and Type Casting

December 13, 2025•13 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript type assertions visualization
Type Assertions and Type Casting

Type Assertions and Type Casting#

Type assertions let you tell TypeScript "trust me, I know what I'm doing" about a value's type. They're essential for working with DOM elements, API responses, and situations where you know more about the type than TypeScript can infer. However, they bypass type checking, so understanding when and how to use them safely is crucial.

Key Distinction: TypeScript type assertions are compile-time only—they don't change the runtime value. They're not the same as type casting in other languages like C# or Java.

What Are Type Assertions?

Type assertions tell the TypeScript compiler to treat a value as a specific type:

// TypeScript thinks this is HTMLElement (generic)
let element = document.getElementById("myInput");
 
// We assert it's specifically an HTMLInputElement
let input = element as HTMLInputElement;
 
// Now we can access input-specific properties
console.log(input.value); // ✓ Works

Important points:

  • Assertions don't change the runtime value
  • They only affect TypeScript's type checking
  • You're telling TypeScript to trust your judgment
  • If you're wrong, you'll get runtime errors

The as Syntax

Basic as Syntax

The as keyword is the modern way to write type assertions:

// Basic syntax
let value = someValue as Type;
 
// Examples
let str = unknownValue as string;
let num = anyValue as number;
let user = data as User;

Common uses:

// DOM elements
let button = document.querySelector(".submit") as HTMLButtonElement;
let input = document.getElementById("email") as HTMLInputElement;
 
// API responses
let response = await fetch("/api/user");
let data = (await response.json()) as User;
 
// Narrowing types
let value: string | number = getValue();
let str = value as string; // Asserting it's a string

Why as is Preferred

The as syntax is recommended because:

1. Works in JSX/TSX files:

// ✓ Works in .tsx files
let element = (<div>Hello</div>) as JSX.Element;
 
// ✗ Angle brackets conflict with JSX
// let element = <JSX.Element><div>Hello</div>;

2. More readable:

// ✓ Clear and readable
let value = someValue as string;
 
// ✗ Less clear
let value = <string>someValue;

3. Consistent with const assertions:

// Uses as syntax
let config = { port: 3000 } as const;

Best Practice: Always use the as syntax unless you're maintaining legacy code. It's clearer, works everywhere, and is the recommended style.

Angle-Bracket Syntax

The angle-bracket syntax is the older form of type assertions:

// Angle-bracket syntax
let value = <Type>someValue;
 
// Examples
let str = <string>unknownValue;
let num = <number>anyValue;
let user = <User>data;

Problems with angle-bracket syntax:

// ✗ Doesn't work in .tsx files (conflicts with JSX)
let element = <HTMLDivElement>document.getElementById("app");
 
// ✗ Can be confused with generics
let result = <Array<number>>[1, 2, 3]; // Confusing!
 
// ✓ as syntax is clearer
let element = document.getElementById("app") as HTMLDivElement;
let result = [1, 2, 3] as Array<number>;

Deprecation Note: While angle-bracket syntax still works in .ts files, it's not recommended. Stick with as syntax for consistency.

Assertions vs Type Casting

Type assertions in TypeScript are NOT the same as type casting in other languages:

Other languages (C#, Java):

// C# - actually converts the value at runtime
string str = (string)obj; // Runtime conversion
 
int num = (int)3.14; // Converts 3.14 to 3

TypeScript:

// TypeScript - compile-time only, no runtime effect
let value: unknown = "hello";
let str = value as string; // No runtime conversion
 
// The compiled JavaScript is just:
let str = value; // Assertion is removed!

Key differences:

FeatureTypeScript AssertionsOther Language Casting
Runtime effectNoneConverts value
Compile timeChanges typeChanges type
SafetyCan be wrongUsually checked
PerformanceZero costMay have cost

Example showing the difference:

let num = 42;
let str = num as string; // TypeScript: no error at compile time
 
console.log(str.toUpperCase()); // Runtime error! num is still 42

TypeScript compiles to:

let num = 42;
let str = num; // Assertion removed
 
console.log(str.toUpperCase()); // Crashes - 42 doesn't have toUpperCase

Critical Understanding: Type assertions don't perform any runtime conversion or checking. They're purely for TypeScript's type system. If you assert incorrectly, you will get runtime errors.

When to Use Type Assertions

DOM Manipulation

DOM APIs return generic types, but you often know the specific element type:

// getElementById returns HTMLElement | null
let input = document.getElementById("username");
input.value = "Alice"; // ✗ Error: Property 'value' does not exist on type 'HTMLElement'
 
// Assert the specific type
let input = document.getElementById("username") as HTMLInputElement;
input.value = "Alice"; // ✓ Works
 
// More examples
let button = document.querySelector(".submit") as HTMLButtonElement;
let canvas = document.getElementById("canvas") as HTMLCanvasElement;
let select = document.querySelector("select") as HTMLSelectElement;
let textarea = document.querySelector("textarea") as HTMLTextAreaElement;

With null checking:

let input = document.getElementById("username") as HTMLInputElement | null;
 
if (input) {
  input.value = "Alice"; // Safe - checked for null
}
 
// Or use optional chaining
(document.getElementById("username") as HTMLInputElement | null)?.focus();

Working with unknown

Type assertions help narrow unknown to specific types:

function parseJSON(json: string): unknown {
  return JSON.parse(json);
}
 
let data = parseJSON('{"name": "Alice", "age": 30}');
 
// Must assert or narrow the type
let user = data as { name: string; age: number };
console.log(user.name); // ✓ Works
 
// Better: validate first, then assert
function isUser(value: unknown): value is { name: string; age: number } {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    "age" in value
  );
}
 
if (isUser(data)) {
  console.log(data.name); // Type-safe!
}

API Responses

Assert API responses to expected types:

interface User {
  id: number;
  name: string;
  email: string;
}
 
async function fetchUser(id: number): Promise<User> {
  let response = await fetch(`/api/users/${id}`);
  let data = await response.json();
  return data as User; // Assert the shape
}
 
// Usage
let user = await fetchUser(1);
console.log(user.name); // ✓ TypeScript knows the type

Better approach with validation:

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value &&
    typeof (value as User).id === "number" &&
    typeof (value as User).name === "string" &&
    typeof (value as User).email === "string"
  );
}
 
async function fetchUser(id: number): Promise<User> {
  let response = await fetch(`/api/users/${id}`);
  let data = await response.json();
 
  if (isUser(data)) {
    return data;
  }
 
  throw new Error("Invalid user data");
}

Type Narrowing

Use assertions to narrow union types when you know the specific type:

type Shape = Circle | Square | Triangle;
 
function processShape(shape: Shape) {
  if ("radius" in shape) {
    let circle = shape as Circle;
    console.log(circle.radius);
  }
}
 
// Or with type predicates (better)
function isCircle(shape: Shape): shape is Circle {
  return "radius" in shape;
}
 
function processShape(shape: Shape) {
  if (isCircle(shape)) {
    console.log(shape.radius); // No assertion needed
  }
}

Double Assertions

Sometimes you need to assert through an intermediate type:

// Direct assertion may fail if types are too different
let num = 42;
let str = num as string; // ✗ Error: Conversion may be a mistake
 
// Double assertion (escape hatch)
let str = num as unknown as string; // ✓ Works (but dangerous!)

When you might need double assertions:

interface OldUser {
  name: string;
}
 
interface NewUser {
  firstName: string;
  lastName: string;
}
 
let oldUser: OldUser = { name: "Alice Smith" };
 
// Direct assertion fails - types are too different
// let newUser = oldUser as NewUser; // Error
 
// Double assertion
let newUser = oldUser as unknown as NewUser;

Danger Zone: Double assertions are a red flag. They bypass all type safety. Use them only as a last resort and document why they're necessary.

Const Assertions

as const creates readonly literal types:

// Without as const
let config = { port: 3000, host: "localhost" };
// Type: { port: number; host: string }
 
// With as const
let config = { port: 3000, host: "localhost" } as const;
// Type: { readonly port: 3000; readonly host: "localhost" }

Use cases:

// 1. Literal arrays
let colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]
 
// 2. Configuration objects
const API_CONFIG = {
  baseURL: "https://api.example.com",
  timeout: 5000,
  retries: 3,
} as const;
 
// 3. Enum-like objects
const STATUS = {
  PENDING: "pending",
  ACTIVE: "active",
  COMPLETED: "completed",
} as const;
 
type Status = (typeof STATUS)[keyof typeof STATUS];
// Type: "pending" | "active" | "completed"
 
// 4. Tuple types
let point = [10, 20] as const;
// Type: readonly [10, 20] (not number[])

Benefits of const assertions:

  • Creates literal types instead of primitive types
  • Makes everything readonly
  • Prevents accidental mutations
  • Better type inference
// Without as const
let directions = ["north", "south", "east", "west"];
let first = directions[0]; // Type: string
 
// With as const
let directions = ["north", "south", "east", "west"] as const;
let first = directions[0]; // Type: "north"

Non-Null Assertions

The ! operator asserts that a value is not null or undefined:

// TypeScript thinks this might be null
let element = document.getElementById("myElement");
// Type: HTMLElement | null
 
// Non-null assertion
let element = document.getElementById("myElement")!;
// Type: HTMLElement
 
// Use the element without checking
element.textContent = "Hello"; // ✓ No error

Common uses:

// Array access
let items = ["a", "b", "c"];
let first = items[0]!; // Assert it exists
 
// Object property
interface User {
  name?: string;
}
 
let user: User = { name: "Alice" };
let name = user.name!; // Assert it's defined
console.log(name.toUpperCase());
 
// Function return
function findUser(id: number): User | undefined {
  // ...
}
 
let user = findUser(1)!; // Assert we found the user
console.log(user.name);

Use Sparingly: Non-null assertions disable safety checks. Use them only when you're absolutely certain the value exists. Otherwise, check explicitly with if statements or optional chaining.

Better alternatives:

// ✗ Non-null assertion (risky)
let element = document.getElementById("myElement")!;
element.textContent = "Hello";
 
// ✓ Null check (safe)
let element = document.getElementById("myElement");
if (element) {
  element.textContent = "Hello";
}
 
// ✓ Optional chaining (concise and safe)
document.getElementById("myElement")?.textContent = "Hello";

Practical Examples

Form Handling

interface FormData {
  username: string;
  email: string;
  age: number;
}
 
function handleSubmit(event: Event) {
  event.preventDefault();
 
  // Assert event target is a form
  let form = event.target as HTMLFormElement;
 
  // Get form elements
  let username = form.elements.namedItem("username") as HTMLInputElement;
  let email = form.elements.namedItem("email") as HTMLInputElement;
  let age = form.elements.namedItem("age") as HTMLInputElement;
 
  let formData: FormData = {
    username: username.value,
    email: email.value,
    age: parseInt(age.value, 10),
  };
 
  console.log(formData);
}
 
// Better with validation
function getFormData(form: HTMLFormElement): FormData | null {
  let username = form.elements.namedItem("username") as HTMLInputElement | null;
  let email = form.elements.namedItem("email") as HTMLInputElement | null;
  let age = form.elements.namedItem("age") as HTMLInputElement | null;
 
  if (!username || !email || !age) {
    return null;
  }
 
  return {
    username: username.value,
    email: email.value,
    age: parseInt(age.value, 10),
  };
}

Event Handling

function handleClick(event: MouseEvent) {
  // Assert event target is a button
  let button = event.target as HTMLButtonElement;
  button.disabled = true;
 
  // Better: check the type first
  if (event.target instanceof HTMLButtonElement) {
    event.target.disabled = true;
  }
}
 
function handleKeyPress(event: KeyboardEvent) {
  // Assert target is an input
  let input = event.target as HTMLInputElement;
  console.log("Input value:", input.value);
}
 
// Generic event handler with type guard
function isButton(element: EventTarget | null): element is HTMLButtonElement {
  return element instanceof HTMLButtonElement;
}
 
function handleEvent(event: Event) {
  if (isButton(event.target)) {
    event.target.disabled = true; // Type-safe!
  }
}

Local Storage

interface Settings {
  theme: "light" | "dark";
  fontSize: number;
  notifications: boolean;
}
 
function saveSettings(settings: Settings) {
  localStorage.setItem("settings", JSON.stringify(settings));
}
 
function loadSettings(): Settings | null {
  let data = localStorage.getItem("settings");
 
  if (!data) {
    return null;
  }
 
  // Parse and assert type
  let settings = JSON.parse(data) as Settings;
 
  // Better: validate the structure
  if (isValidSettings(settings)) {
    return settings;
  }
 
  return null;
}
 
function isValidSettings(value: unknown): value is Settings {
  return (
    typeof value === "object" &&
    value !== null &&
    "theme" in value &&
    "fontSize" in value &&
    "notifications" in value &&
    (value as Settings).theme in ["light", "dark"] &&
    typeof (value as Settings).fontSize === "number" &&
    typeof (value as Settings).notifications === "boolean"
  );
}

Third-Party Libraries

// Untyped library
declare const untypedLib: any;
 
interface LibraryResult {
  data: string[];
  status: number;
}
 
function useLibrary() {
  // Assert the return type
  let result = untypedLib.getData() as LibraryResult;
  console.log(result.data);
}
 
// Better: create proper type definitions
declare module "untyped-lib" {
  export function getData(): LibraryResult;
}
 
// Now no assertion needed
import { getData } from "untyped-lib";
let result = getData(); // Type is LibraryResult

Dangers of Type Assertions

Type assertions bypass type safety and can lead to runtime errors:

1. Asserting incorrect types:

let num = 42;
let str = num as any as string;
console.log(str.toUpperCase()); // Runtime error!

2. Asserting without validation:

interface User {
  name: string;
  email: string;
}
 
let data = JSON.parse(apiResponse) as User;
console.log(data.name.toUpperCase()); // Crashes if name doesn't exist

3. Creating impossible states:

let value: string | number = "hello";
let num = value as number; // Compiles but wrong!
console.log(num.toFixed(2)); // Runtime error!

4. Masking real type errors:

function process(data: string) {
  // ...
}
 
let value = 42;
process(value as any); // Hides the real error

Remember: Every type assertion is a potential runtime error. Use them sparingly and only when necessary. Always prefer type guards and validation over assertions.

Best Practices

1. Prefer type guards over assertions:

// ✗ Avoid
let element = document.getElementById("app") as HTMLDivElement;
 
// ✓ Better
let element = document.getElementById("app");
if (element instanceof HTMLDivElement) {
  // Use element safely
}

2. Validate before asserting:

// ✗ Risky
let user = data as User;
 
// ✓ Safe
function isUser(value: unknown): value is User {
  // Validation logic
}
 
if (isUser(data)) {
  let user = data; // No assertion needed
}

3. Use const assertions for literal types:

// ✓ Good
const config = { port: 3000 } as const;
 
// Instead of
const config: { port: 3000 } = { port: 3000 };

4. Document why assertions are needed:

// ✓ Good - explains the reasoning
// API always returns this shape, but has no types
let user = apiResponse as User;
 
// Library incorrectly types this as any
let result = libraryCall() as SpecificType;

5. Avoid double assertions:

// ✗ Last resort only
let value = something as unknown as SomeType;
 
// ✓ Better - fix the types properly

6. Use non-null assertions sparingly:

// ✗ Risky
let element = document.getElementById("app")!;
 
// ✓ Better
let element = document.getElementById("app");
if (!element) throw new Error("Element not found");

Common Mistakes

Mistake 1: Over-using assertions:

// ✗ Bad - unnecessary assertions
let name = user.name as string;
let age = user.age as number;
 
// ✓ Good - let TypeScript infer
let name = user.name;
let age = user.age;

Mistake 2: Asserting without understanding:

// ✗ Bad - blindly asserting to fix errors
let value = someFunction() as any as Whatever;
 
// ✓ Good - understand the types
let value = someFunction();
// Fix the actual type issue

Mistake 3: Using assertions instead of proper types:

// ✗ Bad
function process(data: any) {
  let user = data as User;
}
 
// ✓ Good
function process(data: User) {
  // No assertion needed
}

Mistake 4: Asserting in the wrong direction:

// ✗ Error - can't assert more specific to more general
let specific: "hello" = "hello";
let general = specific as string; // Unnecessary!
 
// Widening happens automatically
let general: string = specific; // ✓ Works

Mistake 5: Ignoring null/undefined:

// ✗ Bad - assumes element exists
let input = document.getElementById("username") as HTMLInputElement;
input.value = "test"; // Crashes if element doesn't exist
 
// ✓ Good - handle null case
let input = document.getElementById("username") as HTMLInputElement | null;
if (input) {
  input.value = "test";
}

Next Steps

You now understand type assertions and how to use them safely! Here's what to explore next:

Practice These Concepts:

  • Use type guards instead of assertions where possible
  • Add const assertions to your configuration objects
  • Validate API responses before asserting types
  • Review existing assertions in your code

Continue Learning:

  • Type guards and type predicates
  • User-defined type guards
  • Discriminated unions
  • Advanced type narrowing techniques

Congratulations! You've mastered type assertions in TypeScript. Remember: assertions are a tool of last resort. Always prefer type guards, validation, and proper typing. Use assertions wisely and your code will be safer and more maintainable.


Questions about type assertions? Struggling to decide between assertions and type guards? Share your scenario in the comments and let's discuss the best approach!