Casting and Type Checking

Vaibhav • September 10, 2025

In the previous article, we explored how polymorphism allows objects to behave differently based on their runtime type. Now we turn to a closely related topic: casting and type checking. These are essential tools for working with polymorphic objects, especially when you need to convert between types or verify an object’s compatibility before performing operations. In this article, we’ll walk through the different kinds of casting in C#, how to check types safely, and how to avoid common pitfalls when working with inheritance and interfaces.

Why casting matters in polymorphism

When you work with polymorphic objects - for example, treating a Dog as an Animal - you often need to cast between types. Casting allows you to access members that are specific to a derived type, or convert between interfaces and concrete classes. But casting must be done carefully, because not all conversions are valid at runtime.

class Animal
{
    public void Speak()
    {
        Console.WriteLine("Animal sound");
    }
}

class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine("Woof!");
    }
}

Suppose you have an Animal reference that actually points to a Dog object. You can cast it back to Dog to access Bark():

Animal pet = new Dog();
pet.Speak(); // OK

Dog dog = (Dog)pet;
dog.Bark(); // OK

This cast works because pet actually refers to a Dog. But if it referred to a different subclass, the cast would fail at runtime.

Types of casting in C#

C# supports several ways to cast between types. Each has its own behavior and use case:

1. Explicit casting (unsafe)

This uses the cast operator (Type). It throws an exception if the cast is invalid.

Animal pet = new Animal();
Dog dog = (Dog)pet; // Runtime error: InvalidCastException

Use this only when you’re sure the object is of the target type. Otherwise, prefer safer alternatives.

2. Safe casting with as

The as operator returns null if the cast fails, instead of throwing an exception.

Animal pet = new Animal();
Dog dog = pet as Dog;

if (dog != null)
{
    dog.Bark();
}
else
{
    Console.WriteLine("Not a dog");
}

This is safer and more readable, especially when working with interfaces or unknown types.

3. Type checking with is

The is operator checks whether an object is of a given type. It returns a boolean.

if (pet is Dog)
{
    ((Dog)pet).Bark();
}

This pattern is common when you need to verify the type before casting. It avoids runtime errors.

Pattern matching with is

Modern C# supports pattern matching with is, allowing you to combine type checking and casting in one step.

if (pet is Dog dog)
{
    dog.Bark();
}

This syntax checks if pet is a Dog, and if so, declares a new variable dog with the correct type. It’s concise and safe.

Use pattern matching with is whenever possible. It’s safer than explicit casting and more readable than separate checks.

Casting with interfaces

Casting is especially useful when working with interfaces. You might receive an object as an interface type and need to access its concrete members.

interface IShape
{
    double Area();
}

class Circle : IShape
{
    public double Radius;
    public Circle(double radius) { Radius = radius; }

    public double Area() => Math.PI * Radius * Radius;

    public void Draw()
    {
        Console.WriteLine("Drawing circle...");
    }
}

Suppose you have an IShape reference:

IShape shape = new Circle(5);
Console.WriteLine(shape.Area()); // OK

Circle circle = shape as Circle;
circle?.Draw(); // Safe cast

This allows you to access Draw(), which is not part of the interface. But be careful - not all IShape objects are Circles.

Casting and inheritance hierarchies

Inheritance hierarchies often involve multiple levels. You can cast up and down the hierarchy, but only when the actual object supports it.

class Vehicle
{
    public void Start() => Console.WriteLine("Starting vehicle");
}

class Car : Vehicle
{
    public void Drive() => Console.WriteLine("Driving car");
}

class SportsCar : Car
{
    public void Turbo() => Console.WriteLine("Turbo boost!");
}

You can cast a SportsCar to Car or Vehicle safely:

SportsCar sc = new SportsCar();
Car c = sc; // Upcast
Vehicle v = sc; // Upcast

But downcasting requires caution:

Vehicle v2 = new Car();
SportsCar sc2 = v2 as SportsCar;

if (sc2 != null)
{
    sc2.Turbo();
}
else
{
    Console.WriteLine("Not a sports car");
}

This cast fails because v2 is actually a Car, not a SportsCar. Always check before casting down.

Casting and collections

When working with collections of base types or interfaces, you may need to cast individual items to access specific behavior.

List shapes = new List
{
    new Circle(2),
    new Circle(3)
};

foreach (IShape shape in shapes)
{
    if (shape is Circle c)
    {
        c.Draw();
    }
}

This pattern is common in UI frameworks, game engines, and data processing pipelines. It allows you to treat items generically while still accessing specific behavior when needed.

Avoiding casting altogether

In many cases, you can avoid casting by designing your interfaces and base classes carefully. If a method is needed by all consumers, include it in the interface. If a property is shared across types, define it in the base class.

interface IShape
{
    double Area();
    void Draw(); // Add to interface
}

Now you don’t need to cast to access Draw(). This simplifies your code and reduces the risk of runtime errors.

Design interfaces to include all necessary behavior. Avoid casting unless you truly need access to type-specific members.

Common mistakes and how to avoid them

Casting is powerful, but it’s easy to misuse. Here are some common mistakes:

1. Casting without checking: Always verify the type before casting. Use is or as to avoid exceptions.

2. Assuming interface types are always a specific class: Interfaces can be implemented by many classes. Don’t assume a specific implementation.

3. Overusing casting: If you find yourself casting frequently, reconsider your design. You may need better interfaces or base classes.

4. Ignoring null results from as: Always check for null after using as. Otherwise, you risk NullReferenceException.

C# also supports dynamic typing, which defers type checking to runtime. It’s powerful but should be used sparingly in statically typed codebases.

Summary

Casting and type checking are essential tools for working with polymorphic objects in C#. You’ve learned how to use explicit casting, safe casting with as, type checking with is, and pattern matching. You’ve seen how to cast between interfaces and classes, navigate inheritance hierarchies, and avoid common mistakes. By mastering these techniques, you’ll write safer, more flexible code that handles dynamic behavior with confidence.

In the next article, we’ll explore the Object Class - the root of all types in C#, and how its methods like ToString(), Equals(), and GetHashCode() shape object behavior across the language.