📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How does type inference work in C#? When should you use var, target-typed new and inferred lambda types, and when does explicit typing read better?

Type inference in C# 14: var and its rules, anonymous types, target-typed new and collection expressions, generic method inference, natural lambda types, and the readability and LINQ traps that guide when to be explicit.

Junior Theory Beginner csharp lambdas target-typed-new type-inference types var

🎯 Key points

  • var is compile-time inference: the variable is still statically and strongly typed
  • var needs an initialiser and cannot be initialised with null alone; it is required for anonymous types
  • Target-typed new (C# 9) and collection expressions (C# 12) infer the type from the declaration instead
  • Generic method type arguments are inferred from arguments (Max(3, 7) gives T = int)
  • Lambdas have a natural type since C# 10 (var f = (int x) => x * 2 is Func<int, int>)
  • var infers the exact static type, which matters: IQueryable<T> versus IEnumerable<T> changes where a LINQ query runs
  • Literal inference: var x = 0 is int, var p = 9.99 is double, not decimal
  • Use var when the type is obvious from the right-hand side; be explicit when it is not, and enforce the team style with .editorconfig (IDE0007 / IDE0008)

⏱ 30-second answer

Type inference lets the compiler work out a type from context. var takes the static type of the initialiser, so the variable is still strongly typed; it is required for anonymous types. Target-typed new() and collection expressions (List<int> xs = [1, 2]) go the other way, taking the type from the declaration. Generic arguments are inferred from method arguments, and since C# 10 lambdas have a natural type. I use var when the type is obvious from the right-hand side and write the type when it is not, or when the exact type changes behaviour, like IQueryable<T> versus IEnumerable<T> in EF Core.

🗣 Full answer (senior)

How I would answer this in an interview

Type inference means the compiler works out the static type for you at compile time. var is still strongly and statically typed — it is not dynamic and not JavaScript's var. The real question is readability: inference should remove noise, never hide information the reader needs.

1. The forms of inference in C#

Feature Example Inferred from
Implicitly typed locals (C# 3) var orders = new List<Order>(); The initialiser
Generic method type inference Max(3, 7) → Max<int> Argument types
Lambda parameter types orders.Where(o => o.Total > 0) Target delegate type
Natural type of lambdas (C# 10) var parse = (string s) => int.Parse(s); → Func<string,int> Lambda signature
Target-typed new (C# 9) List<Order> orders = new(); Declared target type
Target-typed conditional (C# 9) int? x = flag ? 1 : null; Target type
default literal (C# 7.1) CancellationToken ct = default; Target type
Collection expressions (C# 12) List<int> ids = [1, 2, 3]; Target type
Implicitly typed arrays var a = new[] { 1, 2, 3 }; Best common element type
out var, deconstruction, foreach (var ...) foreach (var (key, value) in dict) Method/enumerator signature

2. Rules for var

  • Only for locals (including for, foreach, using, out var) — not fields, parameters or return types.
  • Must have an initialiser, and it cannot be null alone (var x = null; is an error).
  • The type is fixed at compile time: var count = 0; count = "x"; does not compile.
  • With nullable reference types enabled, var declares a nullable reference type (var name = GetName(); is string?), but flow analysis tracks the actual null-state, so you rarely notice.

3. When I use var

The type is obvious from the right-hand side, or naming it adds noise:

var customer = new Customer("Ada");                       // type stated on the right
var lookup = new Dictionary<string, List<OrderSummary>>(); // avoid repeating a long name
var query = db.Orders.Where(o => o.Status == Paid);       // IQueryable<Order> - name is noise
var summary = orders.Select(o => new { o.Id, o.Total });  // anonymous type - var is REQUIRED
foreach (var (sku, qty) in basket) { ... }

4. When explicit typing reads better

  • The type is not obvious and matters to the reader: decimal total = CalculateTotal(); vs var total = CalculateTotal(); (is it double? decimal? Money?).
  • Numeric literals: var price = 19.99; is a double — for money write decimal price = 19.99m;.
  • You want an interface or base type deliberately: IReadOnlyList<Order> orders = repo.GetAll(); documents intent and restricts what you can call.
  • LINQ materialisation boundaries where IEnumerable vs IQueryable matters: IQueryable<Order> q = ... makes it obvious the query is still server-side.
  • async results: var result = GetOrderAsync(id); without await is a Task<Order>, a classic bug that explicit typing makes obvious.

5. Target-typed new and collection expressions

Target-typed new() is the mirror image of var: type on the left, inferred on the right. It shines for fields and properties, where var is not allowed:

private readonly Dictionary<Guid, OrderState> _states = new();
private readonly List<string> _errors = [];   // C# 12 collection expression

I avoid it when the target is far away (return new(...); from a method whose return type is several lines up) or in arguments where readers cannot tell what is being created (Process(new(), new(5));).

6. Team conventions

The choice is mostly style, so it belongs in .editorconfig, enforced at build time, not in code review debates:

csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion

7. A production example

A bug I have seen more than once: var orders = _repository.GetOrdersAsync(customerId); followed by if (orders == null) and passing orders around. Because there was no await, orders was a Task<List<Order>>; it compiled because the next method accepted object for logging, and the log showed System.Threading.Tasks.Task1[...]. Analyser CS4014 does not fire when the task is assigned. Writing List

🔍 What the interviewer is checking

A quick fluency check with a trap for the unwary.

Green flags

  • Clearly separates var (static) from dynamic.
  • Mentions anonymous types, target-typed new and natural lambda types.
  • Knows the IQueryable<T> versus IEnumerable<T> trap.
  • Has a pragmatic readability rule.

Red flags

  • "var is like JavaScript's var."
  • Dogmatic "always var" or "never var" without reasons.
  • Unaware that var x = 9.99 is a double.

📚 Learn in depth

1. var is compile-time only

var total = 10m;

compiles to exactly the same IL as decimal total = 10m; — the local slot is typed [System.Runtime]System.Decimal. There is no runtime cost, no reflection, no late binding. Contrast with dynamic, which compiles to object plus DLR call sites.

Because the type is decided once, inference can lock in a narrower type than you intended:

var items = new List<Order>();   // List<Order>, not IList<Order>
var x = 5;                       // int - later x = 5_000_000_000 won't compile
var shape = new Circle();        // Circle - cannot later assign a Square

2. How generic method type inference works

For Max(3, 7.5), the compiler runs a two-phase algorithm from the specification:

  1. Phase 1: for each argument, infer bounds for type parameters from the argument type (int and double become candidate bounds for T).
  2. Phase 2: fix each type parameter to the best type among its bounds that all others convert to — here double, since int converts implicitly to double.

If no single best type exists (e.g. Max("a", 5)), inference fails with CS0411 and you must specify Max<object>(...).

Lambdas participate in phases: input types are inferred first from other arguments, then the lambda body is bound to infer output types. That is why Select(o => o.Total) works: TSource is known from the source, then TResult is inferred from the lambda body.

Inference never uses the return type or the variable being assigned to: T Create<T>() cannot be called as Order o = Create();.

3. Natural types for lambdas and method groups (C# 10)

var parse = (string s) => int.Parse(s);     // Func<string, int>
var log = Console.WriteLine;                // error: ambiguous overloads
var now = DateTime.UtcNow.ToString;         // Func<string> if unambiguous
var maybe = string? (bool b) => b ? "y" : null;  // explicit return type

The compiler picks Func<...>/Action<...> when they fit, or synthesises an internal delegate type when they don't (e.g. ref parameters). This also made minimal APIs possible: app.MapGet("/orders/{id}", (int id, IOrderService svc) => ...) infers a delegate with full parameter metadata for binding.

C# 12 added default parameter values in lambdas, and C# 14 allows parameter modifiers (ref, out, scoped) without explicit types.

4. Best common type for arrays and conditionals

  • new[] { 1, 2, 3 } → int[]; new[] { 1, 2.5 } → double[]; new[] { "a", 1 } → error (no best type; object is not chosen automatically).
  • flag ? new Circle() : new Square() — before C# 9 an error; with target typing, Shape s = flag ? new Circle() : new Square(); works because the target type is known.
  • Collection expressions ([1, 2, 3]) have no natural type — var x = [1, 2, 3]; does not compile, by design, because the right collection type depends on the target.

5. var and nullable reference types

With NRT enabled, var locals are declared as nullable (string?), deliberately. The idea: the variable's declared type allows null, but the flow state after var s = "hello"; is not-null, so no warnings. This avoids spurious warnings when a variable is later assigned a maybe-null value. It means var can never be used to force non-nullability.

6. Readability research and guidance

Microsoft's .NET runtime coding guidelines use var only when the type is explicitly named on the right side (new, explicit cast) — the runtime team optimises for reviewers reading diffs without IDE support. Many application teams use var more widely, relying on IDE inlay hints. Both are defensible; the senior point is to pick a rule, encode it in .editorconfig, and apply it consistently, with exceptions for numerics, interfaces, and async results.

Try it yourself

  1. Hover over var in var q = db.Orders.Where(...) and then add .AsEnumerable() — see how the inferred type changes and what that means for SQL translation.
  2. Write var price = 9.99; and pass it to a method taking decimal.
  3. Try var nums = [1, 2, 3]; and read the error.
  4. Configure the .editorconfig rules above with :warning and build.

💻 Code

namespace Types;

public sealed record Order(int Id, decimal Total);

public static class Inference
{
    // Fields cannot use var; target-typed new keeps them short
    private static readonly Dictionary<string, List<Order>> ByCustomer = new();

    public static void Main()
    {
        var orders = new List<Order> { new(1, 120m), new(2, 40m) };   // obvious type
        int[] ids = [1, 2, 3];                                         // collection expression needs a target
        decimal threshold = 100m;                                      // explicit for money

        // Generic inference: TResult = decimal
        var totals = orders.Select(o => o.Total);

        // Anonymous type requires var
        var summary = new { Count = orders.Count, Sum = orders.Sum(o => o.Total) };
        Console.WriteLine(summary);                                    // { Count = 2, Sum = 160 }

        // Natural lambda type (C# 10)
        var isBig = (Order o) => o.Total > threshold;                  // Func<Order, bool>
        Console.WriteLine(orders.Count(isBig));

        // Literal trap
        var price = 9.99;                                              // double!
        Console.WriteLine(price.GetType().Name);                       // Double

        Console.WriteLine($"{ids.Length} {totals.Sum()} {ByCustomer.Count}");
    }

    // Keep EF Core queries as IQueryable<T> so filters run in SQL
    public static IQueryable<Order> BigOrders(IQueryable<Order> source, decimal min)
        => source.Where(o => o.Total >= min);
}

⚠ Common mistakes

1. Treating var as dynamic typing

What happens: developers from JavaScript assume var variables can change type, or avoid var believing it is slower. Why it is wrong: var is fully static; the IL is identical to an explicit declaration. Do instead: explain and use it as a readability tool only.

2. var with numeric literals for money

What happens: var price = 19.99; or var rate = 0.1;. Why it is wrong: these are double, introducing binary rounding into financial code. Do instead: use explicit decimal with the m suffix.

3. Forgetting await behind var

What happens: var order = _repo.GetAsync(id); and later order.Total. Why it is wrong: order is a Task<Order>; depending on usage it either fails to compile far away or silently logs/serialises a Task. Do instead: await on the same line, and enable analysers such as VSTHRD and CA2012 where relevant.

4. Hiding the IQueryable / IEnumerable boundary

What happens: var orders = db.Orders.Where(...); passed to helpers that call .AsEnumerable() or .ToList() at different points. Why it is wrong: readers cannot see where SQL ends and in-memory processing starts, leading to whole-table loads. Do instead: type the boundary explicitly when it matters (IQueryable<Order>), and materialise deliberately.

5. var with non-obvious method results

What happens: var result = Process(input); where result could be a tuple, a result type or a bool. Why it is wrong: reviewers in a pull request (without IDE hints) cannot tell what they are reading. Do instead: use explicit types when the method name does not make the type obvious, or improve the method name.

6. Locking in a concrete type unintentionally

What happens: var shape = new Circle(); then later wanting to assign a Square. Why it is wrong: var infers the most specific type of the initialiser. Do instead: declare the abstraction explicitly: Shape shape = new Circle();.

7. Overusing target-typed new() in arguments

What happens: Send(new(), new("x", 5), new()); Why it is wrong: the reader cannot see which types are created without navigating to the method signature. Do instead: use target-typed new for declarations where the type is visible on the left; name types in arguments.

8. Expecting inference from the return type

What happens: Order o = Deserialize(json); with T Deserialize<T>(string json). Why it is wrong: C# never infers type arguments from the assignment target; CS0411. Do instead: specify the type argument explicitly: Deserialize<Order>(json).

9. Arguing about var in code review

What happens: repeated review comments about style. Why it is wrong: it wastes time and causes inconsistency. Do instead: agree a convention, encode it in .editorconfig with severity, and let dotnet format and the build enforce it.

10. Using var for anonymous-type alternatives in public flows

What happens: anonymous types created with var and passed around via object or dynamic. Why it is wrong: anonymous types are internal and cannot be named outside the method, so they leak as object and lose type safety. Do instead: use a record or tuple when data must leave the method.

🏛 Senior insight: trade-offs

Type inference is a readability tool; seniors care about where it hides important types.

Situation Hidden type that matters
EF Core query composition IQueryable<T> vs IEnumerable<T> (SQL vs memory)
Async call without await Task<T> vs T
Numeric literals double vs decimal, int vs long
foreach (var x in dataTable.Rows) object from non-generic collections
Natural lambda types Func<> vs Expression<Func<>> (only expression trees translate to SQL)
  • Pick a team convention and enforce it with .editorconfig and dotnet format so reviews focus on behaviour.
  • Target-typed new() and collection expressions make explicit declarations short, removing most arguments for var in fields and properties.
  • During code review, hover or use inlay hints to check inferred types in critical paths.

🔗 Follow-ups the interviewer may ask

  1. When: The candidate states that var is still strongly typed. Why: The classic confusion: var is static inference, dynamic is run-time binding.
    How to answer

    dynamic tells the compiler to skip type checking and resolve members, operators and overloads at run time. Under the hood the variable is just object; the compiler emits call sites that use the C# runtime binder on the DLR, which caches the binding for each run-time type. Compared with var (static inference) and object (static, needs casts), dynamic moves errors to run time as RuntimeBinderException, removes IntelliSense, is slower, and does not work with Native AOT. I only use it for COM or Office interop, scripting hosts or quick prototypes; for JSON I'd use JsonNode or typed records.

  2. When: The candidate mentions var x = 9.99 being a double. Why: Literal inference (double vs decimal) matters for money.
    How to answer

    C# has integral types (byte to long, plus nint, Int128), binary floating point (float, double, Half) and decimal. I use int by default, long for IDs and counters that can pass 2.1 billion, double for measurements and science, and decimal for money because it is base-10 and exact for values like 0.1. Integer overflow is unchecked by default, so int.MaxValue + 1 silently wraps; I use checked where wrapping would be a bug. Doubles never throw: dividing by zero gives Infinity or NaN, and Math.Round defaults to banker's rounding.

↩ Usually asked after

More in Types

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me