Back to Blog
C#

C# Pattern Matching Type Conversion: Type Patterns

c# pattern matching type conversion: Learn how to use C# pattern matching for type conversion with type patterns, switch expressions, and guards. Avoid unsafe casts an...

pattern matchingtype patternsswitch expressionsis expressiontype safety
Diagram showing a C# type pattern matching a base type to a derived type with a checkmark.

When you need to check an object's type and then use it as that type, C# pattern matching provides a safer alternative to explicit casts. The is expression with a type pattern, and switch expressions with type patterns, let you combine type testing and conversion in one step. This article covers the syntax, behavior, and practical use of C# pattern matching type conversion.

Understanding Type Patterns

A type pattern tests whether an input matches a specific type. In C#, you write it as input is Type variable. If the input is of that type, the pattern matches and the variable is assigned the input cast to that type. This is different from a traditional as cast, which returns null when the conversion fails. With a type pattern, the is expression returns false and the variable is not assigned.

object value = "hello"; if (value is string text) { Console.WriteLine(text.Length); // text is string }

Here, text is only assigned when value is a string. If value were an int, the if block would be skipped. This eliminates the need for a separate as cast followed by a null check.

Using is Expression for Type Testing

The is expression has been extended to support type patterns, which makes it the most direct way to perform type conversion in a conditional. The pattern variable is scoped to the block where it is used, so you can safely reference it without additional null checks.

public void Process(object data) { if (data is int number) { Console.WriteLine($"Integer: {number}"); } else if (data is string text) { Console.WriteLine($"String: {text}"); } }

This pattern works with any type, including interfaces and base classes. The variable is of the matched type, so you can call members that are specific to that type without casting again.

Switch Expressions and Property Patterns

Switch expressions take pattern matching further by allowing you to combine type patterns with property patterns and guards. A switch expression evaluates an input and returns a value based on the first matching pattern. This is often more concise than a series of if statements.

public string Describe(object obj) => obj switch { int i => $"Integer {i}", string s => $"String {s}", _ => "Unknown" };

The _ pattern is the discard pattern, which matches any input. It acts as a default case. Switch expressions require exhaustive handling, so you must include a discard or a pattern that covers all remaining possibilities.

Property patterns allow you to match on properties of the type after the type check. For example, you can match a DateTime and also check its Year property in one pattern.

public string DescribeDate(object obj) => obj switch { DateTime dt when dt.Year > 2000 => "After 2000", DateTime dt => "Before or equal 2000", _ => "Not a date" };

Here, the when clause adds an additional condition. The type pattern DateTime dt matches first, and then the guard evaluates dt.Year. If the guard fails, the next pattern is tried.

Recursive Patterns and Nested Types

Pattern matching is recursive: you can nest patterns inside other patterns. This is useful when you need to inspect the structure of an object. For instance, you can match a List<int> and also check its count in one pattern.

public string DescribeList(object obj) => obj switch { List<int> list when list.Count > 0 => $"List with {list.Count} items", List<int> list => "Empty list", _ => "Not a list" };

You can also use positional patterns for tuples and records. This allows you to deconstruct and match in a single step.

public string DescribePoint(object obj) => obj switch { (int x, int y) => $"Point ({x}, {y})", _ => "Not a point" };

This pattern matches any Tuple<int,int> or a record with two int properties, depending on the context. Recursive patterns reduce the need for nested if statements and make the code more declarative.

Combining with when Clauses for Complex Conditions

Type patterns are most powerful when combined with when guards. A guard is a boolean expression that must be true for the pattern to match. This lets you filter on values without writing separate if blocks.

public string Classify(object obj) => obj switch { int n when n > 0 => "Positive", int n when n < 0 => "Negative", int n => "Zero", _ => "Not an integer" };

Guards are evaluated only after the type pattern matches. This means the variable n is already typed as int, so you can use its members directly. Guards can also call methods or combine multiple conditions.

One common mistake is to use a guard that accesses a member that might not exist for the matched type. Since the type pattern ensures the variable is of the matched type, this is safe. However, if you use a guard on a base type pattern, the guard must handle the possibility that the actual runtime type might be a derived type with different behavior.

Performance Considerations and Runtime Behavior

Pattern matching is implemented in the runtime and generally does not incur significant overhead compared to manual type checks and casts. The compiler may optimize patterns into efficient type tests and branch tables. However, there are some nuances.

When you use a type pattern, the runtime performs a type check and an implicit cast. This is similar to an is check followed by a cast, but the compiler can sometimes generate more efficient code because it knows the variable is already typed. In switch expressions, the compiler may generate a jump table for sealed types, but for open hierarchies it may use a sequence of type checks.

One performance consideration is the order of patterns. If you have many patterns, the compiler evaluates them in order. Placing more specific patterns first can reduce the number of checks. For example, match string before object if you expect many strings. However, the difference is usually negligible unless the switch is in a hot path.

Another consideration is that pattern matching creates a new variable binding for each pattern. This is a compile-time feature and does not allocate at runtime. The variable is just a reference to the original object, so no extra memory is used.

Common Pitfalls and How to Avoid Them

A frequent mistake is to use a type pattern with a nullable type. For example, int? is a value type, and pattern matching against int? behaves differently than matching against int. Consider this code:

object value = (int?)null; if (value is int? nullable) { Console.WriteLine(nullable.HasValue); // false }

The pattern matches because value is a boxed int? that is null. If you want to match only non-null integers, use int instead:

if (value is int number) { // Only matches when value is a non-null int }

Another pitfall is using as and then checking for null when pattern matching would be clearer. The as operator returns null if the conversion fails, but it also returns null if the input is null. This can lead to ambiguity. Pattern matching with a type pattern does not match null, so it is more explicit.

object value = null; if (value is string text) { // This block is not executed }

If you need to handle null separately, you can use the null pattern explicitly:

if (value is null) { // Handle null } else if (value is string text) { // Handle string }

When to Choose Pattern Matching Over Traditional Casts

Pattern matching is not always the right choice. If you are certain about the type and want to throw an exception if the conversion fails, a direct cast (Type)obj is appropriate. If you want to return null when the conversion fails, as is suitable. Pattern matching is best when you need to conditionally handle multiple types or when you want to combine type checks with additional conditions.

For example, if you have a method that accepts an object and must handle several known types, a switch expression with type patterns is more readable than a series of if statements with as and null checks. It also ensures that you do not forget a type, because the compiler can warn about non-exhaustive switches.

In code that runs frequently, measure the impact if you suspect pattern matching is a bottleneck. The runtime is optimized, but the order of patterns and the complexity of guards can affect performance. Use a profiler to confirm before rewriting to manual checks.

Advanced Usage: Combining Patterns with Records and Tuples

Records and tuples work naturally with pattern matching. You can deconstruct a record in a positional pattern and match on its properties at the same time. This is especially useful for domain models where you need to branch on the shape of data.

public record Point(int X, int Y); public string DescribePoint(object obj) => obj switch { Point(0, 0) => "Origin", Point(0, _) => "On Y axis", Point(_, 0) => "On X axis", Point(x, y) => $"Point ({x}, {y})", _ => "Not a point" };

The positional pattern Point(0, 0) matches a Point record with X and Y both zero. The discard _ in a positional pattern matches any value. This eliminates the need for property checks inside the guard.

This approach is declarative and reduces the chance of logic errors. It also works with tuples, so you can match on (int, int) without defining a record.

Pattern matching is a core feature in modern C#. It helps you write type conversion logic that is safe, concise, and maintainable. By using type patterns, switch expressions, and guards, you can replace verbose casting code with clear, intention-revealing patterns.

c# pattern matching type conversion: Practical Usage and Cod | RYUSLOG DEV