Skip to content

Generics

E# generics are reified: every instantiation is a real closed type or method at runtime, never an erased object. Option<int> is a distinct closed generic struct; List<Point> holds Points by their actual representation. This page specifies parameterization, the open/closed distinction, how type arguments are inferred, and how a generic type conforms to interfaces.

A generic entity introduces type parameters in angle brackets after its name:

Generics = "<" identifier { "," identifier } ">" .

They appear on struct, class, union / ref union, interface, and func:

struct Pair<A, B> { first: A, second: B }
class Box<T> { v: T init(x: T) { self.v = x } func get() -> T = self.v }
union Tree<T> { leaf(value: T), node(left: *Tree<T>, right: *Tree<T>) }
interface IMap<K, V> { func get(k: K) -> V }
func identity<T>(value: T) -> T = value

A type-parameter name follows the type-name casing convention (PascalCase: T, TValue) and is in scope throughout the declaration it heads. enum is never generic — it is an integral type.

Each instantiation is its own CLR closed type; there is no type erasure. A generic argument that is a user type stays that type through the metadata, never boxed to object: a struct field of type T substituted with int is laid out as an int, and match / equality / field access on a closed instantiation see the concrete type. Generics nest — Box<Box<int>> is a closed type whose payload is itself a closed Box<int>, and a field may be a nested construction like Dictionary<string, List<T>>, closed when the enclosing type is. The cost model is the CLR’s: reference instantiations share code, value instantiations are specialized.

A bare generic name (Pair, no arguments) denotes the open definition; it appears only where an open type is meaningful — the type’s own body (its field and method signatures reference the open parameters) and as the target of an instantiation. A name with arguments (Pair<int, string>) is a closed type, and is what values, fields, parameters, returns, and locals are typed by. Resolving a bare generic name in value or type position yields the open definition; supplying arguments closes it.

A generic name is closed by supplying one type argument per declared parameter; the count of arguments is the arity the use site demands (below). Each argument is itself a type — a primitive, a user type, another closed generic (Box<Box<int>>), a tuple, a pointer, or a delegate type.

Two closed types are the same type exactly when they close the same generic definition with the same type arguments, compared by ordinal type identity: Pair<int, string> denotes one type wherever it appears, and Pair<int, int> is a different type from Pair<string, int>. Because instantiations are reified, each closed type carries its own metadata and — for value instantiations — its own layout, independent of every other instantiation of the same definition.

Types are keyed by name and arity, mirroring the CLR’s Name`N convention. The consequences:

  • a generic struct Foo<A, B> (arity 2) and a non-generic static Foo (arity 0) coexist under the one name Foo. The standard library uses exactly this to pair the Result<TValue, TError> value type with a Result static factory class (Result.Ok / Result.Error) — struct Tag<T> alongside static Tag resolves t.v and Tag.id() with no collision;
  • two generic types of the same name and different arity coexist — Cell<A> and Cell<A, B> are distinct types (Cell`1 and Cell`2);
  • only a genuine same-name and same-arity redeclaration is an error (ES2152).

Every use site resolves by the arity it demands: a bare name → arity 0, an instantiation Foo<…> → the count of arguments.

A generic type that refers to itself does so through a pointer, the same rule the value/reference split imposes on non-generic types (Type system → value types). A recursive union or struct carries its self-reference as *T:

union Tree<T> { leaf(value: T), node(left: *Tree<T>, right: *Tree<T>) }

A value struct that contains itself by value — directly (next: Node) or through a generic wrapper that holds it by value (children: List<Node>) — is ill-formed (ES2002); break the cycle with *T (next: *Node, children: List<*Node>). A class is heap-native and may hold itself by reference without a pointer.

A method or function may carry its own type parameters, independent of any on its enclosing type. On a generic type, the receiver’s parameters ride on the declaring type and the method’s remaining parameters become the method’s own:

class Box<T> {
v: T
init(x: T) { self.v = x }
func count<U>(other: U) -> int = 3 // U is the method's own parameter
}
// b.count<string>("x") → Box`1<int>::count<string>(string) -> int

A generic method shows the same split: in func (w: Wrap<T>) mapped<T, U>(f: Func<T, U>) -> Wrap<U>, T is pinned by the Wrap<T> receiver and U is the method’s own (Functions → methods). It emits as Wrap`1<T>::mapped<U>(Func<T, U>) -> Wrap<U> — a generic method on a generic type.

A generic type conforms to an interface by naming it after :, exactly as a non-generic type does (Type system → interfaces); conformance is nominal. The interface may be parameterized by the type’s own parameters, by a closed instantiation, or be non-generic:

interface IBox<T> { func get() -> T }
class Box<T> : IBox<T> { v: T init(x: T) { self.v = x } func get() -> T = self.v }
interface IMap<K, V> { func get(k: K) -> V }
class One<K, V> : IMap<K, V> { val: V init(v: V) { self.val = v } func get(k: K) -> V = self.val }

This holds for a struct, a class, a BCL generic interface (Ver<T> : IComparable<Ver<T>>), and a non-generic interface whose members are independent of the type parameter (Box<T> : ITagged). A value struct conforming to an interface boxes at the interface boundary — passing Bag<int> where ICount<int> is expected boxes the value — which is the same value-through-interface rule as a non-generic struct (Type system → boxing).

A class may extend a generic base, closing it over type arguments written after :. The arguments are the base’s, in the order the base declares them — they need not match the derived type’s own parameters:

open class Animal<T> { tag: T init(t: T) { self.tag = t } }
class Dog<T> : Animal<T> { init(t: T) : base(t) { } } // base closed over the derived's own T
open class Box<T> { value: T init(v: T) { self.value = v } }
class IntBox : Box<int> { init(v: int) : base(v) { } } // base closed over a concrete argument
open class Pair<X, Y> { fst: X snd: Y init(f: X, s: Y) { self.fst = f self.snd = s } }
class Rev<A, B> : Pair<B, A> { init(f: B, s: A) : base(f, s) { } } // arguments reordered

The emitted extends clause and the : base(...) constructor call are both hosted on the closed base instance — Dog<int> extends Animal<int>, IntBox extends Box<int>, and Rev<int, string> extends Pair<string, int> — so a derived value is assignable to the closed base (Dog<int> is an Animal<int>), exactly as reification requires; an open generic base would be invalid metadata. : base(args) resolves against the base’s constructor with matching arity, its parameters substituted through the same closure, so base(t) above calls Animal`1<int>::.ctor(T). A generic base carries its arguments; a non-generic base (class TagAttribute : Attribute) names no arguments and closes nothing. As with any inheritance the base must be the first entry after : and declared open or abstract (Declarations → class).

This is the same closed-base wiring the reified ref union subclass relies on: Expr_add : Expr and a generic Box_full<T> : Box<T> extend their reified base over its arguments through this rule (Declarations → reference union).

E# provides no syntax to declare variance on a type parameter — there is no in / out annotation — so an E# generic interface is invariant in its parameters: IBox<Derived> is not an IBox<Base>. Variance is a property of a generic interface’s own metadata, so when E# conforms to or consumes a variant BCL interface — the covariant IEnumerable<out T>, the contravariant IComparer<in T> — that interface’s declared variance applies, because it travels with the interface, not with E#‘s use of it.

A generic call supplies its type arguments explicitly or has them inferred. Explicit arguments are written in angle brackets at the call (identity<int>(x), b.count<string>("x"), w.mapped<int, string>(f)) and always work. Inference applies when they are omitted, by three steps:

  1. Receiver inference. For a method call, the receiver’s closed type pins every type parameter that appears in the receiver position — w : Wrap<int> pins T = int for w.mapped(...); for a BCL extension method, the receiver pins the source element type (xs : List<int> pins TSource = int for xs.Select(...)).
  2. Argument inference. Each remaining parameter is pinned by matching a value argument’s type against the corresponding parameter’s declared type, descending through constructed generics.
  3. Lambda-body inference. A lambda argument participates in both directions: its parameter types bind from the already-pinned parameters (so (x) => … types x as int above), and its body-inferred return type then pins any type parameter that appears only in the lambda’s result position.
let w = Wrap<int> { v: 3 }
let a = w.mapped((x) => x + 5) // T=int (receiver), x:int, body→int, U=int → Wrap<int>
let b = w.mapped((x) => x.ToString()) // U=string → Wrap<string>
let c = w.mapped((x) => x + 5).mapped((y) => y * 2) // inference threads through the chain
let xs = List<int>() // … populated
let n = xs.Select((x) => x + 1).Sum() // Select<int,int> → IEnumerable<int> → Sum() → int
let m = xs.Where((x) => x > 3).Count() // Where<int>(Func<int,bool>) → int

The same algorithm serves a user generic method and a BCL generic extension (Select / Where / Any / All) uniformly. If a type parameter is pinned by neither the receiver, an argument, nor a lambda body, inference fails for that call and the type arguments must be given explicitly. Inference does not flow from the expected result type back into the call.

derive equality and derive debug (Declarations → derive) apply to generic struct. The generated members target the self-instantiation: derive equality on Pair<A, B> implements IEquatable<Pair<A, B>> and compares field-by-field, and distinct closed instantiations are independent — Pair<int, int> equality and Pair<string, string> equality are separate closed methods. derive debug renders the closed value (Pair { first = 3, second = 4 }).

Type parameters are unconstrained (except unmanaged)

Section titled “Type parameters are unconstrained (except unmanaged)”

A type parameter is opaque at the source level: there is no general constraint clause (no where, no <T : I>). Within a declaration a T is used by storing it (a field, a collection, a tuple), passing it, returning it, taking default(T), or comparing it via a derived equality — operations that hold for any type. There is no way to call a T-specific member on a bare T, because nothing has been asserted about T; reach the concrete operations through a closed instantiation, an interface-typed parameter, or a delegate parameter (Func<T, U>). Reification makes this pay off: at the closed instantiation the argument’s real representation and behavior are present.

The one capability bound the current surface accepts is unmanaged:

Generics = "<" TypeParam { "," TypeParam } ">" .
TypeParam = identifier [ ":" Bound ] .
Bound = "unmanaged" .

func writeArray<T: unmanaged>(…) emits the CLR unmanaged constraint (the NotNullableValueType flag plus a System.ValueType modreq(UnmanagedType) constraint, byte-identical to C#‘s where T : unmanaged), so MemoryMarshal.AsBytes<T>, span reinterpretation, and Unsafe.SizeOf<T> type-check against T. It is specified in Low-level & unmanaged. Other type-parameter bounds — the type-set form like func max<T: int | long | double>(…) — remain part of the union-types direction on the roadmap.

default(T) yields the zero value of T: 0 / false / '\0' for the numeric and char primitives, nil for a reference type or *T, and the all-zero value (initobj) for a value struct, tuple, or other value type — including an unconstrained type parameter, where it lowers to initobj on the reified argument rather than a null reference.