How does type inference work for generic record patterns, and what are the rules and limits around inferring type arguments?
answer
- Inference flows from the operand/selector's static type into the record's components
- Box<String> b => Box(var content) infers content as String
- Explicit Box<String>(...) needed when context can't pin the argument
- var avoids repeating long generic type names; inference composes with nesting
- Erasure: type-argument matching is a compile-time notion, raw type at runtime
basics
~20 sWhen a record is generic, like 'Box<T>', you can often write 'Box(var content)' and let the compiler figure out T from the value you're matching. You don't have to spell out 'Box<String>' if the surrounding type already tells the compiler what T is.
solid answer
~50 sRecord patterns support type-argument inference for generic records. If you match a value whose static type fixes the type argument — for example matching a 'Box<String>' — you may write 'Box(var content)' and the compiler infers the record's type argument and the component types from it, so 'content' is typed String without you writing 'Box<String>'. You can still write the explicit form 'Box<String>(String content)' when you want it spelled out or when inference can't determine the argument. Inference flows from the static type of the matched expression (the selector or instanceof operand) through the record's declared component types. Where the compiler cannot pin the type argument from context, you must supply it explicitly. As with all record patterns, each sub-pattern still binds one component, and 'var' both infers and avoids repeating long generic type names.
code
java · 10 linesrecord Box<T>(T content) {}
record Pair<A, B>(A first, B second) {}
static String inspect(Pair<String, Box<Integer>> p) {
// type args inferred from p's static type; var keeps it terse
if (p instanceof Pair(var name, Box(var n))) {
return name + "=" + (n + 1); // name: String, n: Integer
}
return "?";
}go deeper
Aware that records can be generic and that 'var' lets you avoid writing the type, without needing the inference rules.
Can use 'Box(var content)' when the operand type is concrete and knows you can also write the explicit type argument.
Explains that inference flows from the operand/selector static type, when explicit arguments are required, and that var composes with nesting.
Reasons precisely about inference propagation through nested generic components, the erasure boundary and its unchecked implications, and gives API/design guidance on when to favor inference+var vs explicit arguments for clarity and safety.
## Setup: generic records A record can be **generic**, parameterized by a type variable: ```java record Box<T>(T content) {} record Pair<A, B>(A first, B second) {} ``` The component types are written in terms of the type variables (`T`, `A`, `B`). When you have a *value* of such a record, its type argument is concrete — e.g. a `Box<String>` whose `content()` returns a `String`. ## Inference in record patterns When you deconstruct a generic record in a pattern, the compiler tries to **infer** the record's type argument so you don't have to write it. The information comes from the **static type of the matched expression** — the `switch` selector or the left operand of `instanceof`. Example where inference works: ```java static String unbox(Box<String> b) { if (b instanceof Box(var content)) { // T inferred as String from Box<String> return content.toUpperCase(); // content has static type String } return ""; } ``` Here the operand `b` is statically `Box<String>`, so the compiler resolves `T = String`, and the `var content` sub-pattern is typed `String`. You did **not** need `Box<String>(String content)`. ## Explicit type arguments You can always write the type argument explicitly: ```java if (obj instanceof Box<String>(String content)) { ... } ``` This is required (or at least necessary) when the matched expression's static type is too broad to pin the argument — e.g. matching an `Object` against `Box<String>` involves an *unchecked* relationship, and you must state the intended argument. (Recall that, due to **erasure**, the runtime cannot actually verify `Box<String>` vs `Box<Integer>`; the static type is what drives inference, and matching a generic record pattern against a raw or `Object`-typed value carries the usual generics-erasure caveats and may need an explicit argument or yield an unchecked situation.) ## How inference flows 1. Start from the static type of the operand/selector. 2. If that type is the generic record applied to concrete arguments (`Box<String>`), bind the record's type variables to those arguments. 3. Propagate to each component: a component declared `T` becomes `String`; nested generic components are inferred recursively. 4. `var` sub-patterns then take the inferred component type; explicit sub-patterns must be consistent with it. If step 2 cannot resolve the arguments (the operand is raw, `Object`, or an unrelated type), the compiler cannot infer and you must supply the type argument explicitly — or the pattern is rejected. ## Interaction with nesting and `var` Inference composes with nesting: `Pair<String, Box<Integer>>` matched as `Pair(var a, Box(var b))` infers `a` as `String` and `b` as `Integer`. `var` is especially valuable here because the explicit form (`Pair<String, Box<Integer>>(String a, Box<Integer>(Integer b))`) is verbose; `var` keeps the pattern readable while the compiler supplies the precise types. ## Practical guidance - Lean on inference + `var` for terse, refactor-safe deconstruction when the operand's static type already fixes the arguments. - Spell out type arguments when matching from a broader type (`Object`, raw) or when explicitness aids the reader. - Remember erasure: a generic record pattern's type-argument check is a *static/compile-time* notion; at runtime only the raw record type is testable, so matching across an erased boundary follows the same unchecked-warning rules as ordinary generics.
- Why might you be forced to write 'Box<String>(...)' instead of 'Box(...)'?When the matched expression's static type doesn't fix the type argument — e.g. the operand is Object or a raw Box — the compiler can't infer T, so you must state it explicitly (accepting the usual unchecked-generics caveats from erasure).
- Does generic record-pattern matching verify the type argument at runtime?No. Due to type erasure, the runtime only knows the raw record type; the type-argument is a compile-time inference/check. Matching a generic record pattern across an erased boundary follows the same unchecked rules as any generic cast.
saying these in an interview costs you the question
- Believing the runtime can distinguish Box<String> from Box<Integer> in a pattern (erasure forbids it)
- Assuming inference always works even when matching from Object/raw types
- Thinking var changes inference results vs the explicit component type
- Claiming you can never write explicit type arguments in a record pattern