TypeScript casting is a practical way to fix frustrating type errors and safely work with unknown data like JSON responses or form inputs. In this guide, we’ll cover the basics and advanced use cases of type casting and provide some clarity on casting vs. assertion.
TypeScript’s robust type system lets developers define and enforce types for variables, function parameters, return values, and more. Essentially, it does static type checking, and this helps catch and prevent many errors before the code even runs. But let’s be honest, type-related issues can still surprise us, especially when dealing with data that’s unpredictable.
So maybe you’re parsing a JSON response from an API or handling user input from a form, and TypeScript isn’t sure what type it is. The compiler might throw an error, and you feel stuck.
That’s where TypeScript’s casting feature comes in. It resolves these kinds of issues by explicitly telling TypeScript what a type value should be, allowing you to silence confusing type errors and guide the compiler in the right direction. In TypeScript, this process is technically called type assertion, though many developers use the terms “type casting” and “type assertion” interchangeably in everyday coding discussions.
Casting is especially useful with dynamic data or when TypeScript’s inference falls short. In this article, we’ll dive into casting in TypeScript, showing you how and why to use it to fix type mismatches. To follow along, you should have a working knowledge of TypeScript and object-oriented programming.
In TypeScript, casting is a way for developers to tell the compiler to treat a value as a specific type, overriding the inferred type system when the developer has more information than the compiler.
Type casting can happen in one of two ways: it can be implicit, which is when TypeScript handles the operation, or explicit, when the developer handles the conversion. Implicit casting occurs when TypeScript sees a type error and attempts to safely correct it.
Type casting is essential for performing various operations, including mathematical calculations, data manipulation, and compatibility checks. But before you can start using type casting effectively, you’ll need to understand some foundational concepts like subtype and supertype relationships, type widening, and type narrowing.
Editor’s note: This article was updated by Nelson Michael in May 2025 to clarify casting vs. assertion, expand examples with real-world use cases of type casting, and answer some commonly asked questions.
While these two terms are often used interchangeably amongst developers, there is a subtle difference between type assertion and type casting in TypeScript:
as
keyword, like we’ll see belowString()
, Number()
, Boolean()
, etc.The key difference is that type assertion is purely a compile-time construct — it tells TypeScript to treat a value as a certain type without affecting its runtime behavior. Type casting, on the other hand, actually transforms the data and can affect runtime behavior.
One way to classify types is to split them into sub- and supertypes. Generally, a subtype is a specialized version of a supertype that inherits the supertype’s attributes and behaviors. A supertype, on the other hand, is a more general type that is the basis of multiple subtypes.
Consider a scenario where you have a class hierarchy with a superclass called Animal
and two subclasses named Cat
and Dog
. Here, Animal
is the supertype, while Cat
and Dog
are the subtypes. Type casting comes in handy when you need to treat an object of a particular subtype as its supertype or vice versa.
Type widening, or upcasting, occurs when you need to convert a variable from a subtype to a supertype. Type widening is usually implicit, meaning that it is performed by TypeScript, because it involves moving from a narrow category to a broader one. Type widening is safe, and it won’t cause any errors because a subtype inherently possesses all the attributes and behaviors of its supertype.
Type narrowing, or downcasting, occurs when you convert a variable from a supertype to a subtype. Type narrowing conversion is explicit and requires a type assertion or a type check to ensure the validity of the conversion. This process can be risky because not all supertype variables hold values that are compatible with the subtype.
as
operatorThe as
operator is TypeScript’s primary mechanism for explicit type casting. With its intuitive syntax, as
allows you to inform the compiler about the intended type of a variable or expression.
Below is the general form of the as
operator:
value as Type
Here, value
represents the variable or expression you can cast, while Type
denotes the desired target type. By using as
, you explicitly assert that value
is of type Type
.
The as
operator is useful when you’re working with types that have a common ancestor, including class hierarchies or interface implementations. It allows you to indicate that a particular variable should be treated as a more specific subtype. Here’s some code to illustrate:
class Animal { eat(): void { console.log('Eating...'); } } class Dog extends Animal { bark(): void { console.log('Woof!'); } } const animal: Animal = new Dog(); const dog = animal as Dog; dog.bark(); // Output: "Woof!"
In this code, the Dog
class extends the Animal
class. The Dog
instance is assigned to a variable animal
of type Animal
. By using the as
operator, you cast animal
as Dog
, allowing you to access the bark()
method specific to the Dog
class. The code should output this:
You can use the as
operator to cast to specific types. This capability comes in handy when you need to interact with a type that differs from the one inferred by TypeScript’s type inference system. Here’s an example:
function getLength(obj: any): number { if (typeof obj === 'string') { return (obj as string).length; } else if (Array.isArray(obj)) { return (obj as any[]).length; } return 0; }
The getLength
function accepts a parameter obj
of type any
. In the getLength
function, the as
operator casts obj
to a string for any[]
based on its type. This operation gives you access to the length
property specific to strings or arrays, respectively.
Additionally, you can cast to a union type to express that a value can be one of several types:
function processValue(value: string | number): void { if (typeof value === 'string') { console.log((value as string).toUpperCase()); } else { console.log((value as number).toFixed(2)); } }
The processValue
function accepts a parameter value
of type string | number
, indicating that it can be a string or a number. By using the as
operator, you cast value
to string
or number
within the respective conditions, allowing you to apply type-specific operations such as toUpperCase()
or toFixed()
.
as
and <>
in TypeScript?In TypeScript, both as
and <>
are used for type assertions, but they have some important differences:
To start, here’s how their syntax differs:
// Using the "as" syntax const value: any = "hello"; const length = (value as string).length; // Using the angle bracket syntax const length2 = (<string>value).length;
as
syntax works in all TypeScript files (.ts
, .tsx
)<>
syntax doesn’t work in JSX/TSX files because it conflicts with JSX syntaxas
syntax for consistency and compatibilityas
syntax is more flexible with where it can be placed in an expression<>
syntax must be placed at the start of the expression it’s assertingas
, you can chain assertions: (obj as any as MyType)
<>
, chaining is less intuitive: <MyType><any>obj
Do not throw type casting at the smallest error! Type casting is a powerful feature, but you shouldn’t use it casually. It should be applied thoughtfully, usually when you’re confident about the data’s shape, but TypeScript isn’t. Here are some good use cases:
any
In these scenarios, casting helps with TypeScript’s static type system and real-world, often unpredictable, data.
DOM API interactions often require casting because TypeScript can’t determine element types precisely:
// Simple event handling with casting document.querySelector('#loginForm')?.addEventListener('submit', (event) => { event.preventDefault(); // TypeScript doesn't know this is a form element const form = event.target as HTMLFormElement; // Access form elements const emailInput = form.elements.namedItem('email') as HTMLInputElement; const passwordInput = form.elements.namedItem('password') as HTMLInputElement; const credentials = { email: emailInput.value, password: passwordInput.value }; // Process login... }); // Another common DOM casting scenario function handleButtonClick(event: MouseEvent) { const button = event.currentTarget as HTMLButtonElement; const dataId = button.dataset.id; // TypeScript now knows about dataset // Load data based on the button's data attribute loadItemDetails(dataId); }
unknown
, never
, and any
Types in TypeScript?TypeScript provides special types that sometimes require special handling with type assertions.
The unknown
type is safer than any
because it forces you to perform type checking before using the value:
function processValue(value: unknown) { // Error: Object is of type 'unknown' // return value.length; // Correct: Using type checking first if (typeof value === 'string') { return value.length; // TypeScript knows it's a string } // Alternative: Using type assertion (less safe) return (value as string).length; // Works but risky } // A safer pattern with unknown function parseConfig(config: unknown): { apiKey: string; timeout: number } { // Validate before asserting if ( typeof config === 'object' && config !== null && 'apiKey' in config && 'timeout' in config ) { // Now we can safely cast return config as { apiKey: string; timeout: number }; } throw new Error('Invalid configuration'); }
The never
type represents values that never occur. It’s useful for exhaustiveness checking:
type Shape = Circle | Square | Triangle; interface Circle { kind: 'circle'; radius: number; } interface Square { kind: 'square'; sideLength: number; } interface Triangle { kind: 'triangle'; base: number; height: number; } function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.sideLength ** 2; case 'triangle': return (shape.base * shape.height) / 2; default: // This ensures we've handled all cases const exhaustiveCheck: never = shape; return exhaustiveCheck; } } // If we add a new shape type but forget to update getArea: // interface Rectangle { kind: 'rectangle', width: number, height: number } // The function will have a compile error at the exhaustiveCheck line
When dealing with any
, gradual typing can help improve safety:
// External API returns any function externalApiCall(): any { return { success: true, data: [1, 2, 3] }; } // Safely handle the response function processApiResponse() { const response = externalApiCall(); // Check structure before casting if ( typeof response === 'object' && response !== null && 'success' in response && 'data' in response && Array.isArray(response.data) ) { // Now we can safely cast const typedResponse = response as { success: boolean; data: number[] }; return typedResponse.data.map(n => n * 2); } throw new Error('Invalid API response'); }
However, casting comes with risks. Since type assertions and casts override TypeScript’s type checking, incorrect assumptions can result in runtime errors. For example, if you cast a value to a type it doesn’t match, your code may compile, but crash at runtime. That’s why casting should be your last resort, not your first.
Instead of jumping straight to casting, consider these alternatives:
Use typeof
, instanceof
, or custom type guards to help TypeScript infer the correct type.
Without narrowing you might be tempted to cast:
function handleInput(input: string | number){ const value = (input as number) + 1 // This is unsafe if input is actually a string }
With type narrowing (safe and readable):
function handleInput(input: string | number){ if (typeof input === "number"){ return input + 1; // safely inferred as number } return parseInt(input, 10) + 1; }
instanceof
function logDate(value: Date | string) { if(value instanceof Date){ console.log(value.toISOString()); } else{ console.log(new Date(value).toISOString()); } }
type Dog = { kind: "dog"; bark: () => void }; type Cat = { kind: "cat"; meow: () => void }; type Pet = Dog | Cat; function isDog(pet: Pet): pet is Dog { return pet.kind === "dog"; } function handlePet(pet: Pet) { if (isDog(pet)) { pet.bark(); // safely treated as Dog } else { pet.meow(); // safely treated as Cat } }
When working with reusable functions or components, generics can preserve type safety without the need for casting.
Without generics (requires casting):
function getFirst(arr: any): any { return arr[0]; } const name = getFirst(["Alice", "Bob"]) as string; // cast needed
function getFirst<T>(arr: T[]): T { return arr[0]; } const name = getFirst(["Alice", "Bob"]); // inferred as string const age = getFirst([1, 2, 3]); // inferred as number
Generics preserve the data type, so there’s no need for assertions.
If you can model your data accurately from the start (e.g., via interfaces, enums, or discriminated unions), you’ll rarely need to cast at all.
const userData = JSON.parse('{"id": 1, "name": "Jane"}'); const user = userData as { id: number; name: string }; // type cast needed
interface User { id: number; name: string; } function parseUser(json: string): User { const data = JSON.parse(json); // Ideally validate `data` here before returning return data; // if validated, no cast needed }
Here’s a more robust JSON parsing example when working with API responses:
// Define your expected type interface User { id: number; name: string; email: string; preferences: { darkMode: boolean; notifications: boolean; }; } // API response handling async function fetchUser(userId: string): Promise<User> { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); // TypeScript sees this as 'any' // Option 1: Type assertion when you're confident about the structure return data as User; // Option 2: Better approach with validation (recommended) if (isUser(data)) { // Using a type guard function return data; // TypeScript now knows this is User } throw new Error('Invalid user data received'); } // Type guard function function isUser(data: any): data is User { return ( typeof data === 'object' && data !== null && typeof data.id === 'number' && typeof data.name === 'string' && typeof data.email === 'string' && typeof data.preferences === 'object' && typeof data.preferences.darkMode === 'boolean' && typeof data.preferences.notifications === 'boolean' ); }
type Response = | { status: "success"; data: string } | { status: "error"; message: string }; function handleResponse(res: Response) { if (res.status === "success") { console.log(res.data); // safely inferred } else { console.error(res.message); // safely inferred } }
From a performance standpoint, casting has no cost; it exists purely at compile time. But safety and readability are still at stake. Overuse of as
can make your code brittle, hard to refactor, and confusing for future maintainers.
In short, cast only when:
Use it wisely, document it clearly, and revisit it often — especially as your code evolves.
as
operatorWhile the as
operator is a powerful tool for type casting in TypeScript, it has some limitations. One limitation is that as
operates purely at compile-time and does not perform any runtime checks. This means that if the casted type is incorrect, it may result in runtime errors. So, it is crucial to ensure the correctness of the type being cast.
Another limitation of the as
operator is that you can’t use it to cast between unrelated types. TypeScript’s type system provides strict checks to prevent unsafe casting, ensuring type safety throughout your codebase. In such cases, consider alternative approaches, such as type assertion functions or type guards.
as
castingThere are instances when TypeScript raises objections and refuses to grant permission for as
casting. Let’s look at some situations that might cause this.
TypeScript’s static type checking relies heavily on the structural compatibility of types, including custom types. When you try to cast a value with the as
operator, the compiler assesses the structural compatibility between the original type and the desired type.
If the structural properties of the two custom types are incompatible, TypeScript will raise an error, signaling that the casting operation is unsafe. Here’s an example of type casting with structural incompatibility errors using custom types:
interface Square { sideLength: number; } interface Rectangle { width: number; height: number; } const square: Square = { sideLength: 5 }; const rectangle = square as Rectangle; // Error: Incompatible types
TypeScript prevents the as
casting operation because the two custom types, Square
and Rectangle
, have different structural properties. Instead of relying on the as
operator casting, a safer approach would be to create a new instance of the desired type, and then manually assign the corresponding values.
Union types in TypeScript allow you to define a value that can be one of several possible types. Type guards play a crucial role in narrowing down the specific type of a value within a conditional block, enabling type-safe operations.
However, when attempting to cast a union type with the as
operator, it is required that the desired type be one of the constituent types of the union. If the desired type is not included in the union, TypeScript won’t allow the casting operation:
type Shape = Square | Rectangle; function getArea(shape: Shape) { if ('sideLength' in shape) { // Type guard: 'sideLength' property exists, so shape is of type Square return shape.sideLength ** 2; } else { // shape is of type Rectangle return shape.width * shape.height; } } const square: Shape = { sideLength: 5 }; const area = getArea(square); // Returns 25
In the above snippet, you have a union type Shape
that represents either a Square
or Rectangle
. The getArea
function takes a parameter of type Shape
and needs to calculate the area based on the specific shape.
To determine the type of shape
inside the getArea
function, we use a type guard. The type guard checks for the presence of the sideLength
property using the in
operator. If the sideLength
property exists, TypeScript narrows down the type of shape
to Square
within that conditional block, allowing us to access the sideLength
property safely.
Type assertions, denoted with the as
keyword, provide functionality for overriding the inferred or declared type of a value. However, TypeScript has certain limitations on type assertions. Specifically, TypeScript prohibits as
casting when narrowing a type through control flow analysis:
function processShape(shape: Shape) { if ("width" in shape) { const rectangle = shape as Rectangle; // Process rectangle } else { const square = shape as Square; // Process square } }
TypeScript will raise an error because it cannot narrow the type of shape
based on the type assertions. To overcome this limitation, you can introduce a new variable within each branch of the control flow:
function processShape(shape: Shape) { if ("width" in shape) { const rectangle: Rectangle = shape; // Process rectangle } else { const square: Square = shape; // Process square } }
By assigning the type assertion directly to a new variable, TypeScript can correctly infer the narrowed type.
A discriminated union is a type that represents a value that can be of several possibilities. Discriminated unions combine a set of related types under a common parent, where each child type is uniquely identified by a discriminant property. This discriminant property serves as a literal type that allows TypeScript to perform exhaustiveness checking:
type Circle = { kind: 'circle'; radius: number; }; type Square = { kind: 'square'; sideLength: number; }; type Triangle = { kind: 'triangle'; base: number; height: number; }; type Shape = Circle | Square | Triangle;
You’ve defined three shape types: Circle
, Square
, and Triangle
, all collectively forming the discriminated union Shape
. The kind
property is the discriminator, with a literal value representing each shape type.
Discriminated unions become even more powerful when you combine them with type guards. A type guard is a runtime check that allows TypeScript to narrow down the possible types within the union based on the discriminant property.
Consider this function that calculates the area of a shape:
function calculateArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.sideLength ** 2; case 'triangle': return (shape.base * shape.height) / 2; default: throw new Error('Invalid shape!'); } }
TypeScript leverages the discriminant property, kind
, in the switch
statement to perform exhaustiveness checking. If you accidentally omit a case, TypeScript will raise a compilation error, reminding you to handle all possible shape types.
You can use discriminated unions for type casting. Imagine a scenario where you have a generic response
object that can be one of two types: Success
or Failure
. You can use a discriminant property, status
, to differentiate between the two and perform type assertions accordingly:
type Success = { status: 'success'; data: unknown; }; type Failure = { status: 'failure'; error: string; }; type APIResponse = Success | Failure; function handleResponse(response: APIResponse) { if (response.status === 'success') { // Type assertion: response is of type Success console.log(response.data); } else { // Type assertion: response is of type Failure console.error(response.error); } } const successResponse: APIResponse = { status: 'success', data: 'Some data', }; const failureResponse: APIResponse = { status: 'failure', error: 'An error occurred', }; handleResponse(successResponse); // Logs: Some data handleResponse(failureResponse); // Logs: An error occurred
The status
property is the discriminator in the program above. TypeScript narrows down the type of the response
object based on the status
value, allowing you to safely access the respective properties without the need for explicit type checks:
satisfies
operatorThe satisfies
operator was introduced in TypeScript 4.9 to allow you to check whether an expression’s type matches another type without casting the expression. This can be useful for validating the types of your variables and expressions without changing their original types.
Here’s the syntax for using the satisfies
operator:
expression satisfies type
And here’s a program that checks if a variable is greater than five with the satisfies
operator:
const number = 10; number satisfies number > 5;
The satisfies
operator will return true
if the expression’s type matches, and false
if otherwise. It’s a powerful tool for improving the type safety of your TypeScript code.
In data manipulation, you’ll always need to transform data from one type to another, and the two common transformations you will run into are casting a string to a number or converting a value to a string. Let’s look at how to approach each one.
There are several ways to cast a string to a number in TypeScript:
Using the Number()
function:
let numString: string = '42'; let num: number = Number(numString);
Using the unary +
operator:
let numString: string = '42'; let num: number = +numString;
Using parseInt()
or parseFloat()
:
let intString: string = '42'; let int: number = parseInt(intString); let floatString: string = '3.14'; let float: number = parseFloat(floatString); parseInt() and parseFloat() are more flexible as they allow extracting a number from a string that also includes non-numeric characters. Also, it is good to note that all of these methods will yield NaN (Not a Number) if the string cannot be parsed as a number.
String()
You can use the String()
function or the toString()
method to convert a value to a string in TypeScript:
let num: number = 42; let numString: string = String(num); // or let numString2: string = num.toString(); let bool: boolean = true; let boolString: string = String(bool); // or let boolString2: string = bool.toString();
Both String()
and toString()
work on essentially any type and convert it to a string representation.
toString() is a method on the object itself, while String() is a global function. In most cases, they will yield the same result, but toString() allows customizing the string representation by overriding the method on custom types: class CustomType { value: number; constructor(value: number) { this.value = value; } toString() { return `CustomType: ${this.value}`; } } let custom = new CustomType(42); console.log(String(custom)); // Output: [object Object] console.log(custom.toString()); // Output: CustomType: 42
In the above snippet, String(custom)
doesn’t have any special behavior for our CustomType
, whereas custom.toString()
uses our custom implementation.
In this article, you learned about the various ways to perform type casting in TypeScript, including type assertion with the as
operator, type conversion using built-in methods like String()
, Number()
, and Boolean()
, and the subtle differences between type assertion and type casting.
You also learned about concepts like type guards and discriminated unions, which allow you to narrow down the specific type within a union type based on runtime checks or discriminant properties. With these techniques, you can efficiently improve the type safety of your programs and catch potential errors at compile time.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe switch to Go may be a pragmatic move in the short term, but it risks alienating the very developers who built the tools that made TypeScript indispensable in the first place.
JavaScript date handling can be tough. Here are some native Date API tools and specialized libraries to tackle them with.
Walk you through how to set up and use the Tailwind Typography plugin, also known as the @tailwindcss/typography or the prose plugin.
TypeScript adds static typing to JavaScript code, which helps reduce unpredictable behavior and bugs. In the past, TypeScript code couldn’t […]