Skip to main content

Introduction to TypeScript Casting

You're working with data from an API, a DOM query, or a third-party library. TypeScript says the value is unknown or any, but you know exactly what shape it has. So you reach for as and move on.

That works great. Until it doesn't. TypeScript casting (more precisely, type assertion) is a compile-time tool. It tells the compiler to trust you, not check you. Use it correctly and you get clean, type-safe code. Use it carelessly and you've traded a compiler error for a production crash.

This guide covers as, as const, angle-bracket syntax, satisfies, and type guards, so you know which tool to reach for and when.

Casting a Variable to a Specific Type

To cast a variable to a specific type in TypeScript, use the as keyword. Here's an example:

let variable: unknown = 'hello';
let strVariable = variable as string;
console.log(strVariable); // outputs: hello

One thing to be clear on: this doesn't change the variable's actual type at runtime. It only instructs the TypeScript compiler to treat it as a string during compilation. The as keyword is compile-time only. Your JavaScript output won't contain it.

Safe Type Casting in TypeScript

For safe type casting, use the instanceof operator to check a variable's type before casting:

class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}

let variable: unknown = new Person('Alice');
if (variable instanceof Person) {
let person = variable as Person;
console.log(person.name); // outputs: Alice
}

Using runtime checks like instanceof means TypeScript narrows the type automatically inside the if block. TypeScript already knows variable is a Person there, so the explicit as Person cast is almost redundant. But it makes your intent clear when working with external data.

Using Type Assertions for Casting

Type assertions tell TypeScript you know the type of a value better than it does. Use the as keyword for this:

let variable: unknown = { name: 'John' };
let person = variable as { name: string };
console.log(person.name); // outputs: John

Type assertions are particularly useful when working with complex objects or types from third-party libraries. They give TypeScript enough information to provide autocomplete and error checking based on the asserted type.

Handling Incompatible Type Casting

Here's where developers get tripped up. When you cast a string to number with as, TypeScript won't throw at compile time and nothing gets converted at runtime -- the value is still a string. TypeScript just stops checking:

let value: unknown = 'hello';
let num = value as number;

// TypeScript thinks this is a number -- it's not
console.log(typeof num); // 'string', not 'number'
console.log(num.toFixed(2)); // Runtime crash: num.toFixed is not a function

The dangerous part is that TypeScript won't warn you. You've told it to trust you, so it does. For actual type conversion (not just assertion), use the built-in conversion functions:

// Actual runtime conversion
let strValue = '42';
let numValue = Number(strValue); // 42
let alsoNum = parseInt(strValue, 10); // 42

When dealing with external data where runtime failures are possible, a try-catch block handles errors gracefully. Just don't expect as itself to throw anything. It won't.

Choosing Between as and <> for Casting

TypeScript supports two syntaxes for type assertions. In React, prefer as to avoid conflicts with JSX syntax:

// using 'as' keyword (always safe)
let variable: unknown = 'hello';
let strVariable = variable as string;

// using '<>' syntax (breaks in JSX/TSX files)
let strVariable2 = <string>variable;

The as syntax is the modern standard. When working with frameworks like React with TypeScript, the angle-bracket syntax isn't an option in .tsx files.

Casting Objects with Optional Properties

When casting objects with optional properties, check for property existence using the in operator:

interface UserProfile {
name: string;
age?: number;
}

let rawData: unknown = { name: 'John' };
let profile = rawData as UserProfile;

if ('age' in profile) {
console.log(profile.age);
} else {
console.log('age property does not exist'); // outputs: age property does not exist
}

You'll use this when normalizing data from APIs or form submissions where some fields are optional and you need to check before accessing them.

Using Casting for API Response Types

Here's the standard pattern for typing API response data:

interface ApiResponse {
data: { id: number; name: string };
}

fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
let apiResponse = data as ApiResponse;
console.log(apiResponse.data.id); // outputs: 1
console.log(apiResponse.data.name); // outputs: John
});

Keep in mind: as doesn't validate the shape of data at runtime. If the API returns something unexpected, TypeScript won't catch it -- you told the compiler to trust you. When you need actual runtime validation, use a validation library or write guards that verify the structure first.

When working with Convex, you might cast data from queries or use utility types to transform between different representations of your data.

Casting DOM Elements in TypeScript

DOM access is one of the most frequent reasons to reach for as. Methods like querySelector return Element | null, which means TypeScript won't let you access element-specific properties without help:

// TypeScript infers: Element | null
const input = document.querySelector('#username');

// Error: Property 'value' does not exist on type 'Element | null'
console.log(input.value);

// Cast to the specific element type you expect
const inputEl = document.querySelector('#username') as HTMLInputElement;
console.log(inputEl.value); // works

// Safer: use instanceof to let TypeScript narrow the type
const el = document.querySelector('#username');
if (el instanceof HTMLInputElement) {
console.log(el.value); // TypeScript narrows it automatically
}

The instanceof check is safer when the element might not exist or could be the wrong type. The as HTMLInputElement cast is fine when you're confident about the DOM structure. Just know that if the selector returns a <div>, you'll get a runtime error, not a compile-time warning.

as const: Casting to Literal Types

as const is a special form of assertion that tells TypeScript to infer the narrowest possible type for a value and make it readonly:

// Without as const: TypeScript widens to string[]
const directions = ['north', 'south', 'east', 'west'];

// With as const: readonly ['north', 'south', 'east', 'west']
const directions2 = ['north', 'south', 'east', 'west'] as const;

// Useful for config objects where you want literal types preserved
const apiConfig = {
endpoint: '/api/users',
method: 'GET',
} as const;
// type: { readonly endpoint: "/api/users"; readonly method: "GET" }
// Without as const, method would be widened to string -- losing the literal "GET"

This comes up a lot when defining lookup objects or configuration that other parts of your code need to key off of. See the full as const guide for more patterns including tuple types and enum-like objects.

as vs. satisfies: Picking the Right Tool

TypeScript 4.9 introduced satisfies, and it solves a problem that as handles poorly. With as, you override TypeScript's inference completely. The compiler trusts you and stops checking. With satisfies, TypeScript validates your value matches the type while preserving the most specific inferred type:

type Config = {
host: string;
port: number;
};

// as: TypeScript stops checking -- no error even with a typo
const config1 = { host: 'localhost', prot: 3000 } as Config; // silently wrong

// satisfies: TypeScript validates AND preserves literal types
const config2 = { host: 'localhost', port: 3000 } satisfies Config;
// config2.port is typed as 3000 (literal type preserved)

// satisfies catches mistakes that as misses:
const config3 = { host: 'localhost', prot: 3000 } satisfies Config; // Error: 'prot' doesn't exist

Use as when TypeScript can't infer what you already know (external data, DOM queries). Use satisfies when you want TypeScript to validate your code while keeping the most precise types. The full satisfies guide covers more scenarios where it's the better choice.

Double-Casting for Complex Type Conversions

Sometimes you need to convert between two types that TypeScript won't allow directly. This double-casting technique routes through unknown as an intermediate step:

// First cast to unknown, then to the desired type
let originalValue: string = '123';
let numberValue = (originalValue as unknown) as number;

This works because unknown accepts any type, so it acts as a bridge. You bypass TypeScript's safety checks entirely. Use it rarely. If you find yourself needing it often, the type definitions probably need fixing, not the values.

Type Casting with Type Guards

Type guards are often a better call than casting. TypeScript narrows the type automatically based on your runtime check, so you get full type safety without any assertions:

function processValue(value: string | number) {
if (typeof value === 'string') {
// TypeScript knows value is a string here
return value.toUpperCase();
} else {
// TypeScript knows value is a number here
return value.toFixed(2);
}
}

typeof checks like this are preferable to as when you're working with union types. TypeScript tracks the narrowed type through control flow, no trust-me required.

Converting Between Interfaces and Types

When working with interfaces and types that have different shapes, explicit conversion is usually better than casting:

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

type UserDTO = {
userId: number;
userName: string;
}

function convertToDTO(user: User): UserDTO {
return {
userId: user.id,
userName: user.name
};
}

const user: User = { id: 1, name: 'Alice' };
const dto = convertToDTO(user);

This is safer than casting because TypeScript actually verifies the return value matches UserDTO. Casting would just assert. Converting actually checks. This pattern is common when using Convex for data persistence and mapping between stored data and your UI layer.

Common Type Casting Issues and Solutions

Type Casting Arrays

When working with arrays, you might need to cast an entire array or its elements:

// Casting an entire array
const mixedData: any[] = ['a', 'b', 'c'];
const stringArray = mixedData as string[];

// Casting individual elements
const transformedData = mixedData.map(item => item as string);

Casting Third-Party Library Types

When using external libraries, you might need to cast their types to work with your application:

import { SomeExternalType } from 'external-library';

function processExternalData(data: unknown) {
const typedData = data as SomeExternalType;
// Now you can work with the data using the external type
}

You'll reach for this when a library has incomplete type definitions or when you need to adapt their types to fit your own system.

When to Cast vs. When Not To

Here's a quick decision guide:

ScenarioBest approach
TypeScript can't infer an external value's typeas
You want TypeScript to validate a value matches a typesatisfies
You need a literal or readonly typeas const
Narrowing union types based on structureType guard (typeof, instanceof)
Converting data between two different shapesWrite a function, don't cast

The core rule: as bypasses TypeScript's checks entirely. Use it when TypeScript genuinely can't infer what you already know. For everything else, prefer approaches that keep the compiler working for you rather than around you.