Updated on

Type checking tells us what type a value has at run time, and type casting converts a value from one data type to another.

In C#, we check types with is, GetType() and typeof, and we cast with implicit conversions, cast expressions, the as operator and helpers such as Parse() and the Convert class. The two meet when a cast fails, and depending on the syntax we pick, a wrong type gives us a compiler error, an exception, false or null.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

How Does Type Casting Work in C#?

Type casting converts a value of one type into a value of another type. We need it because C# is statically typed: a value keeps the type it was created with, but a variable or a method parameter may expect a different one.

C# gives us two kinds of conversion. An implicit conversion needs no syntax, and the built-in ones always succeed, such as int to long or a Horse to its base class Animal.

An explicit conversion needs a cast expression, such as (int)biggerNumber or (Horse)animal, because it can lose data or fail. A long that doesn’t fit in an int overflows, and casting an Animal that is really a Monkey to Horse throws an InvalidCastException.

The as operator is an explicit cast that returns null instead of throwing. For a string that holds a number, we need a real conversion, so we use Parse(), TryParse() or the Convert class. Our own types can declare implicit and explicit conversion operators.

Implicit conversions

When a numeric type can be safely converted to another numeric type without losing data, the compiler will let us assign the new value without any special syntax.

For instance, we can implicitly assign an integer value to a variable whose numeric type has a wider range of values than the initial type:

int number = 10;
long biggerNumber = number; // biggerNumber contains 10

For reference types, there’s always an implicit conversion to its direct or indirect base types:

Horse horse = new();   // derived type
Animal animal = horse; // assign to the base type

Assigning a derived type to its base type like this is also called upcasting, and our article on upcasting and downcasting in C# goes deeper into both directions.

Cast Expressions

There are situations where a value may be converted to another type but there is a risk of data loss or the conversion may fail for other reasons. In these cases, there will be no implicit conversion in place and we’ll have to use a cast expression:

long biggerNumber = 10;
int number = (int)biggerNumber; // number contains 10

Cast expressions consist of using parenthesis to the left of an expression to enclose the type we want the result of the expression to be converted to. Be aware that, if the value overflows the destination type, you may get the wrong data or an overflow exception depending on your compiler configuration. By default, when biggerNumber holds long.MaxValue, (int)biggerNumber gives us -1 without an error. Inside a checked block, or with <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> in the project file, the same cast throws an OverflowException.

When working with reference types, we need to use cast expressions when converting a base type to a derived type:

Animal animal = new Horse(); // base type variable
Horse horse = (Horse)animal; // assign to derived type

Usually, the compiler tries to catch an invalid cast expression and reports a compile-time error. However, that’s not always possible so, in some cases, we can get an InvalidCastException:

Animal animal = new Monkey();
Horse horse = (Horse)animal; // compiles but throws InvalidCastException at run-time

The “as” Operator

We can use the as operator to perform explicit casts instead of the parentheses syntax. The difference is that the as operator doesn’t throw a run-time exception in case of an invalid cast. In that case, the conversion will simply result in a null value:

Animal animal = new Horse();

Horse? horse = animal as Horse;    // explicit conversion
Monkey? monkey = animal as Monkey; // invalid conversion, monkey contains null

We can use the as operator only with reference or nullable types. It also handles boxing and unboxing, so boxed as int? gives us the int stored in an object. Our boxing and unboxing article explains what happens in that step. The as operator doesn’t consider user-defined conversions, for user-defined conversions use a cast expression.

Conversion Helpers

The Parse()  method will convert a string to another type provided the string has the correct format:

int number = int.Parse("10");      // convert string to integer
bool boolean = bool.Parse("True"); // convert string to boolean

This operation will throw a FormatException at runtime if the string doesn’t have the right format for the conversion.

We can use the TryParse() variant to avoid having to handle exceptions. TryParse() returns a bool value indicating whether the operation succeeded. The converted value will be stored in an output parameter:

var succeed = int.TryParse("10", out var number);
// succeed == true
// number == 10

The Convert class exposes many static type conversion methods between basic types:

decimal fraction = 12.5M;

int integer = Convert.ToInt16(fraction);
string str = Convert.ToString(fraction);
bool boolean = Convert.ToBoolean("true");

Finally, we can use the BitConverter class to convert basic types from and to their basic representation as byte arrays:

int integer = 1024;
float fraction = 12.5F;

var integerAsBytes = BitConverter.GetBytes(integer);
var floatAsBytes = BitConverter.GetBytes(fraction);

var format = "{0,12}{1,15}";
Console.WriteLine(format, "Base Type", "Bytes");
Console.WriteLine(format, "=========", "=====");
Console.WriteLine(format, integer, BitConverter.ToString(integerAsBytes));
Console.WriteLine(format, fraction, BitConverter.ToString(floatAsBytes));

// Base Type         Bytes
// =========         =====
//      1024   00-04-00-00
//      12.5   00-00-48-41

User-Defined Conversions

When creating our own types we can implement implicit and explicit type conversions with the same syntax as operator overloading:

public class Currency
{
    private string _symbol;
    private decimal _value;

    public Currency(string symbol, decimal value)
    {
        _symbol = symbol;
        _value = value;
    }

    public static implicit operator decimal(Currency c) => c._value;
    public static explicit operator Currency(decimal d) => new Currency("$", d);

    public override string ToString() => $"{_symbol}{_value:0.00}";
}

Here, we are creating a custom Currency type and its implicit conversion to the decimal type. Also, we are overloading the explicit cast operator to allow conversions from decimal to Currency using cast expressions:

var currency = new Currency("$", 1950.75M);
decimal decimalValue = currency; // decimalValue contains 1950.75

var anotherDecimal = 775.2M;
var convertedCurrency = (Currency)anotherDecimal; // convertedCurrency prints "$775.20"

How Do We Check the Type of an Object in C#?

We check the type of an object in C# in two ways, and they answer different questions.

The is operator asks whether a value is compatible with a type. animal is Animal is true for a Horse, because a Horse is an Animal. For null, is returns false instead of throwing.

GetType() asks for the exact run-time type. It returns a Type instance, the class that represents a type at run time, and we compare it with the Type instance that typeof(Animal) gives us. For a Horse, animal.GetType() == typeof(Animal) is false, because the exact type is Horse. Calling GetType() on null throws a NullReferenceException.

So is is the everyday check, and with a declaration pattern, if (data is string name), it checks and casts in one step. We reach for GetType() and typeof when only the exact type will do, for example when a derived type must not pass.

The typeof() Operator

The typeof() operator receives the name of a type as a parameter and returns the Type class instance for that type: 

var typeValue = typeof(string);

Also, we can use typeof() with type parameters:

string GetTypeName<T>() => typeof(T).Name;

var typeName = GetTypeName<int>();      // Int32
var typeName2 = GetTypeName<decimal>(); // Decimal

Here, we define a function GetTypeName<T>() that takes a type parameter T and uses typeof() to get the related Type instance. After that, we use the Name property of the Type class to return the type’s name to the caller. Note that the returned names are the .NET type names, Int32 and Decimal, because int and decimal are C# aliases for System.Int32 and System.Decimal.

The GetType() Method

We can call the Object.GetType() method on an instance or value at run-time to get its associated instance of the Type class:

var stringValue = "hello";
var singleValue = 12.5f;

stringValue.GetType(); // String
singleValue.GetType(); // Single

How to Type Check With typeof() And GetType()

Using both typeof() and Object.GetType() we can type check a value at run-time:

object[] values = { 6, 12.5f, 100M, true, "hello" };

foreach (var val in values)
{
    string? message;
    if (val.GetType().Equals(typeof(int)))
    {
        message = $"Value {val} is an integer";
    }
    else if (val.GetType().Equals(typeof(decimal)))
    {
        message = $"Value {val} is a decimal";
    }
    else if (val.GetType().Equals(typeof(float)))
    {
        message = $"Value {val} is a floating-point value";
    }
    else if (val.GetType().Equals(typeof(bool)))
    {
        message = $"Value {val} is a boolean";
    }
    else if (val.GetType().Equals(typeof(string)))
    {
        message = $"Value {val} is a string";
    }
    else
    {
        message = $"Value {val} belongs to an unknown type";
    }

    Console.WriteLine(message);
}

// Value 6 is an integer
// Value 12.5 is a floating-point value
// Value 100 is a decimal
// Value True is a boolean
// Value hello is a string

Here, we start with an array of values of different types. Next, we call the GetType() method on each value to get its associated run-time type definition. After that, we compare that type definition with the result of using the typeof() operator on different type names to determine whether a value is of a certain known type.

Note that we’re using the Equals() method to compare Type instances, but == works just as well. Microsoft Learn’s Type class reference explains why: “A Type object that represents a type is unique”, so two Type references point to the same object only when they represent the same type.

The “is” Operator

A more succinct and safe way to type check is to use the is operator. The is operator takes a value or expression to its left and a type identifier to its right and returns true if the type of the value on the left is compatible with the type on the right:

We declare the values as object here, because with int number the compiler already knows the answer and warns us that the check is pointless:

object number = 16;
object name = "John Smith";

bool isInt = number is int;      // true
bool isString = name is string;  // true
bool isBool = number is bool;    // false

Starting with C# 7, it is possible to perform safe castings using the is operator and declaration patterns:

if (data is string name)
{
    message = $"Hello {name}!";
}

Let’s assume that the data variable is of type Object and we don’t know at run-time what kind of data will it contain. We use the is operator and the declaration pattern to conditionally create a local string variable that will contain the converted data. If the value in the data variable is not a string the conditional expression will resolve to false and the conversion will not take place.

Declaration and type patterns are one part of pattern matching in C#, and a type pattern also works negated, as data is not string. We compare that negated form with != in is not vs !=.

How Do We Cast Without Throwing an Exception in C#?

C# has no TryCast method. A safe cast is written with the as operator or with an is declaration pattern, and both handle a wrong type without an exception.

animal as Horse returns the Horse when the object is one and null when it isn’t, so the next line has to check for null.

animal is Horse horse does the check and the cast in one step. The horse variable is assigned only when the check passes, which makes this the shorter choice inside an if.

Both have limits. as works only when the target is a reference type or a nullable value type, so boxed as int doesn’t compile while boxed as int? does. Neither one runs a user-defined conversion: our Currency stored in an object is never a decimal, even though Currency converts to decimal implicitly.

For text, the safe route is TryParse(), which returns false instead of throwing a FormatException.

Microsoft Learn’s type-testing reference puts the difference in one line: “Unlike a cast expression, the as operator never throws an exception.” The table below lines up five checks and casts from this article by what each does when the type is wrong:

SyntaxWhen it compilesWrong type at run timenull inputRuns user-defined conversions
(Horse)animalwhen an explicit conversion exists, otherwise error CS0030throws InvalidCastExceptionreturns nullyes, when the source or target type has one
animal as Horsetarget is a reference type or a nullable value type (else CS0077), and a reference, boxing, unboxing or nullable conversion exists (else CS0039)returns nullreturns nullno
animal is Horsealways, with warning CS0183 or CS0184 when the compiler already knows the answerreturns falsereturns falseno
animal is Horse horsewhen a value of the static type can be the target type, otherwise error CS8121 (where plain is only warns)returns false, horse is not assignedreturns falseno
animal.GetType() == typeof(Horse)alwaysreturns falsethrows NullReferenceExceptionno

The last column follows from how the compiler picks a user-defined conversion: it goes by the static types. That is why (decimal)currency works, but the same Currency stored in an object throws an InvalidCastException when we cast it to decimal.

Conclusion

In this article, we’ve learned how implicit and explicit type casting works. We’ve also learned about some conversion helper methods and how to implement our custom-type conversions.

Additionally, we’ve seen some examples of how to type-check value types at run-time and we’ve learned what as and is operators are and how to use them, and which of them lets us cast without risking an exception.

Tested with .NET 10.0.10 (SDK 10.0.302) and xUnit 2.9.3.