kotlin-native-nuget Help

Generics

Generic Kotlin classes and functions become generic C# classes and methods, keeping the same type parameter, constraints, and variance.

class Box<T>(val value: T) { init { require(value.toString().isNotEmpty()) { "Box cannot hold a blank value" } } }
using var box = new Box<Cat>(oreo); Cat cat = box.Value;

An exported enum class works as the type argument too, nullable included (new Box<Mood>(Mood.Calm), Box<Mood?>, Helpers.Identity<Mood?>(null)), on a generic class and on a generic method. Kotlin receives the real enum entry, not its ordinal. A value outside the enum's range ((Mood)99) throws a catchable KotlinException at the call.

Type mappings

Kotlin

C#

class<T>

class<T>

sealed class<T>

a generic abstract class, arms on a non-generic holder (Outcome.Ok<T>)

Box<String> as a property, parameter or return

Box<string>, caller-owned when returned

<T : Bound>

where T : Bound

out T/in T on an interface

out T/in T

fun <T> f(value: T): T (top-level)

a generic method

fun <T : Any> describe(value: T): String (top-level)

static string Describe<T>(T value) where T : notnull

fun <T> pick(first: T, second: T): T (top-level)

static T Pick<T>(T first, T second)

inline fun

an ordinary method

inline fun <reified T>

an ordinary generic method

typealias

the aliased type itself, no separate alias type

Nullable properties

A nullable property on a generic class (val x: T?) is T? in C#. At a reference-type instantiation a null read stays null; at a value-type instantiation it collapses to default(T), same as any other unconstrained C# generic. A property that doesn't mention T keeps its own declared type instead, at every instantiation:

class Slot<T>(val value: T) { val previous: T? = null val current: T? = value val label: String = "window sill" val count: Int = 2 var note: String = "Oreo napped here" val keeper: Cat = Cat("Mylo", 7) }
using var stringSlot = new Slot<string>("hi"); Assert.Null(stringSlot.Previous); using var intSlot = new Slot<int>(42); Assert.Equal(0, intSlot.Previous); // default(int), not null string label = intSlot.Label; // string, not T, even though T is int here using Cat keeper = intSlot.Keeper; // Cat, not T intSlot.Note = "Mylo stole the spot";

Nullable type arguments

An unconstrained T (no bound, so its implicit upper bound is Any?) accepts a null argument at a constructor, method parameter, or return, and reads back null, the same way any other nullable handle position does:

using var bowl = new Box<string?>(null); Assert.Null(bowl.Value); using var crate = new Crate<string?>("tuna"); Assert.Null(crate.Pick(null)); Assert.Equal("null:tuna", crate.Describe(null));

A bound rules this out. class Tin<T : Any>(val value: T) renders where T : notnull, so a nullable type argument is a compile-time warning (CS8714) in a nullable-enabled consumer, on top of the runtime failure a null argument would still hit on the Kotlin side. A nullable bound (T : Pet?) keeps its ? and behaves like the unconstrained case.

public class Tin<T> : IDisposable, INugetHandle where T : notnull

The same rule applies to a top-level generic function: Helpers.Identity<int?>(null) and fun <T : Any> handBack(treat: T): T behave the same way as their generic-class counterparts.

If a generic class's property name collides with one of its own type parameter names once both are PascalCased (class Duo<A, B>(val a: A, val b: B), where property A and type parameter A would both render A), the type parameter is renamed in C# (A becomes TA, B becomes TB); the property keeps its ordinary name. A diagnostic about the type parameter still names it the way you wrote it in Kotlin.

public class Duo<TA, TB> : IDisposable, INugetHandle { public Duo(TA a, TB b) { /* ... */ } public TA A { get; } public TB B { get; } }

Constraints

A bound (<T : Pet>) becomes a C# where T : ... clause. Any type assignable to the bound works as the type argument:

class PetBox<T : Pet>(val value: T) { init { require(value.name.isNotBlank()) { "PetBox needs a named pet" } } }
using var oreo = new Cat("Oreo", 9); using var box = new PetBox<Cat>(oreo);

A bound declared in the Kotlin standard library (Comparable<T>, Number, CharSequence) has no C# equivalent, so it is dropped from the where clause and the build reports an INFO_DROPPED_BOUND note. A non-null bound keeps where T : notnull. C# can then pass a type argument Kotlin would reject, which Kotlin refuses when it reads the argument, before the function body or constructor runs. The call throws KotlinInvalidCastException.

class Ranked<T : Comparable<T>>(val value: T) { fun outranks(other: T): Boolean = value > other }
using var oreo = new Ranked<int>(9); Assert.True(oreo.Outranks(4)); // public class Ranked<T> ... where T : notnull

A parameter with several bounds lists every exportable one in the where clause, on a class and on a function alike. The stdlib bounds are dropped as above, so where T : Comparable<T>, T : Pet renders where T : IPet.

class Arena<T>(val champion: T) where T : Pet, T : Trainable { fun challenge(challenger: T): T = if (challenger.tricks > champion.tricks) challenger else champion }
using var oreo = new Performer("Oreo", 3); // Performer is both a Pet and a Trainable using var arena = new Arena<Performer>(oreo); // where T : IPet, ITrainable

A bound on a generic interface keeps its type arguments in the where clause, so class Chain<T> where T : Node<T>, T : Pet over an invariant Node<T> is where T : INode<T>, IPet. A bound C# cannot spell (a use-site projection, or a collection argument) is dropped with INFO_DROPPED_BOUND, and a T outside it throws at the call.

An enum bound

T : Enum<T> binds as where T : struct, global::System.Enum, on a generic class, a generic function and a member's own type parameter. Any exported enum is a valid type argument, and Kotlin runs the enum API (name, ordinal, compareTo) on the real entry:

class Rosette<T : Enum<T>>(val winner: T) { val vacancy: T? = null fun outranks(other: T): Boolean = winner > other fun better(other: T): T = if (other > winner) other else winner } fun <T : Enum<T>> requirePrize(medal: T): T { require(medal.ordinal > 0) { "${medal.name} is no prize" } return medal }
using var oreo = new Rosette<Medal>(Medal.Gold); Assert.True(oreo.Outranks(Medal.Silver)); Assert.Equal(Medal.Gold, oreo.Better(Medal.Silver)); Assert.Null(oreo.Vacancy); // T? is Nullable<Medal> Assert.Equal(Medal.Silver, Medals.RequirePrize(Medal.Silver)); Assert.ThrowsAny<ArgumentException>(() => Medals.RequirePrize(Medal.Bronze)); // BRONZE is no prize

A T with an enum or self-referencing bound also works as a collection element, a map key, and inside a generic class, on a function, a class member or a constructor. T is inferred as usual:

object Judge { fun <T : Enum<T>> sorted(medals: List<T>): List<T> = medals.sortedDescending() fun <T : Enum<T>> pad(medals: List<T?>): List<T?> = medals + null fun <T : Enum<T>> scores(board: Map<T, Int>): Map<T, Int> = board.mapValues { it.value * 2 } } class Sash<T : Enum<T>>(val ribbons: List<T>)
IReadOnlyList<Medal> podium = Judge.Sorted(new[] { Medal.Silver, Medal.Gold }); IReadOnlyList<Medal?> padded = Judge.Pad(new Medal?[] { Medal.Gold, null }); // null stays null IReadOnlyDictionary<Medal, int> doubled = Judge.Scores(new Dictionary<Medal, int> { [Medal.Gold] = 1 }); using var sash = new Sash<Medal>(new[] { Medal.Gold });
  • Rosette<int>, Rosette<string>, a wrapper and Rosette<Medal?> do not compile.

  • C# cannot say "an enum generated from Kotlin", so a .NET enum such as DayOfWeek compiles and throws a catchable NotSupportedException at the call, before anything reaches Kotlin.

  • A T property is settable, as on any generic class.

  • inline fun <reified T : Enum<T>> is skipped with a named SKIPPED_UNSUPPORTED_COMBINATION, because the generated export cannot supply a reified argument. Drop reified, or export a non-generic overload per enum. A suspend or Flow member of an enum-bound class is skipped as on any generic class.

  • A nullable bound (T : Enum<T>?) is dropped from the where clause with INFO_DROPPED_BOUND. T : Pet, T : Enum<T> binds, but no C# enum implements IPet, so nothing satisfies it.

A generic bound from another package

When the bound is declared in a different Kotlin package than the generic class itself, the generated where clause spells it fully qualified (where T : global::TestLibrary.Cat.IPet) instead of a bare name. This only matters if you inspect the constraint through reflection; calling the class works the same either way.

Variance

out T/in T on an interface carries straight through to the C# interface:

interface Readable<out T> { fun read(): T } interface Writable<in T> { fun write(value: T) }
// IReadable<Cat> can be assigned to IReadable<IPet> because T is covariant Assert.True(typeof(IReadable<IPet>).IsAssignableFrom(typeof(IReadable<Cat>)));

Variance declared on a class's own type parameter, as opposed to an interface's, is dropped: C# does not support variance on classes. The class still generates and works, just without out/in on its type parameter.

Writable properties

A var typed by the class's own type parameter gets a C# setter: T, T?, and a List, Set or Map of T. Value types and strings are boxed for the call and the box is released afterwards; an exported class you pass in is borrowed, so it stays yours to dispose.

class Playpen<T>(first: T) { var current: T = first var maybe: T? = null var items: List<T> = listOf(first) }
using var pen = new Playpen<int>(9); pen.Current = 7; pen.Items = new[] { 1, 2, 3 }; using var oreo = new Cat("Oreo", 9); using var cats = new Playpen<Cat>(oreo); cats.Current = oreo; // borrowed, still yours to dispose using var names = new Playpen<string?>("Oreo"); names.Maybe = null;

The same holds for a bounded T (T : Furry), a trampolined T : Enum<T>, and an interface var item: T, which declares T Item { get; set; } on IKeepsake<T>. Playpen<int>.Maybe = null does not compile, because an unconstrained T? is spelled T in C# (see Nullable properties). A generic interface binds at every position, and one you implement in C# crosses to Kotlin and comes back as itself; see A generic interface.

Methods on a generic class

A public method declared on a generic class binds as an instance method on the C# generic carrier, T positions included, the same ADR-062 plan an ordinary class's methods bind on:

open class Crate<T>(val item: T) { fun describe(tag: T): String = "$tag:$item" fun pick(other: T): T = other fun label(prefix: String, count: Int): String = "$prefix-$count:$item" }
using var crate = new Crate<int>(3); Assert.Equal("7:3", crate.Describe(7)); Assert.Equal(7, crate.Pick(7)); Assert.Equal("lot-2:3", crate.Label("lot", 2));

A position that never mentions T (Label above) binds exactly as it would on a non-generic class. A position that does (Describe, Pick) crosses as the same boxed handle a T-typed property already uses: a value type pays one box mint and dispose per call, an exported class instantiation borrows the argument's own live handle and mints nothing on the way in, and a value class mints and disposes a box through its own box/unbox pair, running init at the boundary. A generic one (Box<Sticker<Int>>, or Sticker<T> over a function's own T) uses a single pair for every instantiation, see A generic value class at an erased position. A value class over a sealed type or a Char has a pair too, and so does one over Instant, Duration, Uuid, ByteArray, a collection, a plain interface or another value class, so Box<PawPrint> crosses like Box<CatId>. A Kotlin member that uses a value class with no C# record struct (over a Throwable, and so on) at T is skipped by name, and new Box<V>(v) for such a V written in C# throws NotSupportedException at the call, since an open C# generic has no build-time way to refuse it.

An interface at an erased position

T bound to an exported interface (PetBox<T : Pet>, or the unconstrained T above) accepts your own C# implementation and hands back the SAME instance, not a fresh Kotlin-backed wrapper. A Kotlin-backed value read at T = IPet still materializes as IPet, through a factory the interface itself now carries:

sealed class Dog(string name) : IPet { /* ... */ } using IPet rex = new Dog("Rex"); using var box = new PetBox<IPet>(rex); Assert.Same(rex, box.Value); using var oreo = new Cat("Oreo", 9); using var oreoBox = new PetBox<IPet>(oreo); IPet boxed = oreoBox.Value; // a fresh IPet wrapper over the Kotlin cat, not the same kind of "same instance"

Each crossing mints a bridge handle for your C# object (freed after the call) or reads one back by token (freed by the read); see Implementing a Kotlin interface in C#. T bound to something that isn't your own C# type and also isn't a Kotlin-backed instance Kotlin can reconstruct (a different implementation entirely) throws InvalidCastException; a T = Dog read of a Kotlin-backed value that isn't a Dog throws NotSupportedException naming the missing factory, inherent to erasure. The same identity rule applies to a lambda argument (see Lambdas and callbacks) and to generic functions below.

T is admitted at a parameter, a return, a constructor parameter, or a property (getter and setter), optionally nullable, and as the element of a List, Set or Map at those positions (List<T> binds as IReadOnlyList<T>; see A type parameter as an element). It is refused, named, everywhere else: T nested inside another generic type. A T in a lambda (non-suspend or suspend) and in a Flow parameter binds. A type parameter declared on an interface rather than a class is unaffected by this and keeps its own, unrelated named refusal.

Subclassing a generic base

A class extending an exported generic base spells the closed type argument, and inherits members, including methods, from that closed base:

open class Parcel<T>(val value: T) class NamedParcel(name: String) : Parcel<String>(name) class LabelledCrate(item: String) : Crate<String>(item) { fun describe(tag: Int): String = "#$tag:$item" }
using var parcel = new NamedParcel("Oreo"); Assert.Equal("Oreo", parcel.Value); // inherited from Parcel<string> Assert.IsAssignableFrom<Parcel<string>>(parcel); var labelled = new LabelledCrate("apple"); Assert.Equal("#7:apple", labelled.Describe(7)); // its own declared overload Assert.Equal("ripe:apple", labelled.Describe("ripe")); // inherited from Crate<string>

Because Parcel and Crate are declared open, their generated Dispose() is virtual so a subclass can override it. A subclass declaring its own overload of a name it also inherits from the generic base keeps exactly its own declared member; the base's substituted overload is not re-declared alongside it, and C# overload resolution then picks between the two exactly as Kotlin does. Generic subclasses (class Sub<T> : Base<T>(...)) are not supported yet; declare the member directly on the closed subclass instead.

An unexported class between the subclass and the generic base (even a generic Keel<U> : Crate<U>) changes nothing for the consumer: the exported subclass extends the generic base directly and declares only what it or the unexported class declares.

open class Crate<T>(val item: T) { open suspend fun load(): String = "crate loaded $item" } open class Keel : Crate<Int>(7) { // outside the export set open fun weigh(): String = "keel:$item" } class Barge : Keel() { override fun weigh(): String = "barge:$item" override suspend fun load(): String = "barge loaded $item" }
await using var barge = new Barge(); // public class Barge : Crate<int> Crate<int> crate = barge; Assert.Equal(7, crate.Item); // inherited, not restated on Barge Assert.Equal("barge:7", barge.Weigh()); Assert.Equal("barge loaded 7", await barge.LoadAsync());

Crate<T> has LoadAsync too (a generic class binds its suspend members), and Barge inherits it; the override runs on the Kotlin side.

A generic class at a member position

A closed instantiation of an exported generic class (Box<String>, Box<Cat>, Box<Box<Int>>, Box<String>?) binds anywhere a class does: a property (a var too), a method return or parameter, a constructor parameter, a companion, an object, the top level, an extension receiver, a List element, a suspend result or parameter, and a Flow or StateFlow element.

class BoxShelf { val label: Box<String> = Box("Oreo") var favourite: Box<Cat> = Box(Cat("Mylo")) fun peek(box: Box<String>): String = box.value fun maybe(present: Boolean): Box<String>? = if (present) Box("here") else null fun nested(): Box<Box<Int>> = Box(Box(7)) } val topBox: Box<String> = Box("top shelf") val Box<Int>.doubled: Int get() = value * 2
using var shelf = new BoxShelf(); using Box<string> label = shelf.Label; // a new wrapper on every read Assert.Equal("Oreo", shelf.Peek(label)); // passed in: borrowed, still yours to dispose using var oreo = new Cat("Oreo"); using var boxed = new Box<Cat>(oreo); shelf.Favourite = boxed; // the setter borrows too Assert.Null(shelf.Maybe(false)); using Box<Box<int>> outer = shelf.Nested(); using Box<int> inner = outer.Value; // Value is another wrapper to dispose using Box<string> top = BoxShelves.TopBox; using var four = new Box<int>(4); Assert.Equal(8, four.Doubled); // an extension on Box<int>

A Box<T> Kotlin hands back is yours to dispose, and every read is a new wrapper over the same Kotlin object. A Box<T> you pass in is borrowed: the call neither keeps nor disposes your wrapper. The type argument can be a primitive, String, an exported class, enum, interface or value class, a nullable of those (Box<String?>), or another closed instantiation.

A top-level function that returns an instantiated generic class works the same way, and can take enum, primitive, String and exported class parameters. A nullable one keeps its null, and overloads bind side by side:

fun treatsFor(mood: Mood): Box<Int> = Box(mood.ordinal + 1) fun treatsFor(name: String): Box<Int> = Box(name.length) fun snackPlan(mood: Mood?, naps: Int?): Box<Int> = Box(((mood?.ordinal ?: -1) + 1) * 100 + (naps ?: -1))
using Box<int> treats = CatMoodTrackerKt.TreatsFor(Mood.Grumpy); using Box<int> plan = CatMoodTrackerKt.SnackPlan(null, 0); // Mood? and int?

A generic abstract class returns the same way. The value is an internal subclass that forwards to the Kotlin object, as described in An abstract class as a return type.

These are skipped, named, instead of generating C# that fails to compile:

  • A type argument the bridge cannot read back: a collection (Box<List<Int>>), ByteArray, a lambda, an object or Unit. A Flow or StateFlow argument binds; see A flow as a type argument.

  • A use-site out projection (Box<out Cat>), since C# has none for a generic class. A star projection binds: Box<*> is Box<object?>, read at the parameter's bound (see Any). A star-projected generic value class result is a type parameter instead; see A star-projected result. So is a star-projected extension receiver; see Projection receivers.

  • An inner class of a generic owner (Tin<Int>.Latch). A generic interface (Shelf<String>) binds at every position; see A generic interface.

  • An outer type that is not an exported class (Pair<Int, Int>).

A refused argument is reported as SKIPPED_UNSUPPORTED_TYPE naming the member, for example for a Box<List<Int>> return: "its generic class <package>.Box has no C# spelling here: the erased wire cannot read its type argument List<Int> ".

A MutableStateFlow<Box<Int>> binds as a KotlinMutableStateFlow<Box<int>> with a settable .Value, CompareAndSet and Update (see Settable .Value); the written box is borrowed, so you still dispose it. A C# class can implement a Kotlin interface that has a member returning Box<Int> or taking one.

A generic sealed hierarchy binds at the same positions; see A generic sealed hierarchy. Unlike a generic class, a generic sealed arm also binds its callback members; see Members of a generic arm.

A flow as a type argument

Box<Flow<Int>> and Box<StateFlow<Int>> bind as Box<KotlinFlow<int>> and Box<KotlinStateFlow<int>>, including as a top-level function return. box.Value is a holder you await foreach and dispose, and every read of .Value returns a new one. The holder is read-only, and Box<MutableStateFlow<T>> binds as a read-only KotlinStateFlow<T>. See A flow inside a generic class for the example, disposal rules and what is skipped.

Generic functions

fun <T> identity(value: T): T = value fun <T> wrapInBox(value: T): Box<T> = Box(value) fun <T> unwrapBox(box: Box<T>): T = box.value fun <T : Pet> adoptPet(pet: T): T = pet inline fun <reified T : Pet> groomPet(pet: T): T = pet
using Box<int> box = Helpers.WrapInBox<int>(99); Assert.Equal(99, box.Value); Assert.Equal(99, Helpers.UnwrapBox(box)); // a passed Box<T> is borrowed

A constrained generic function carries the same where clause as a constrained class. Inlining only matters inside Kotlin. A reified T reifies at its declared bound (Pet for groomPet), and at Any? when it has none, whatever type argument C# passes.

An unconstrained T bound to an exported interface gets the same identity treatment as the generic-class case above: the object arm now writes through the same shared marshalling helper every other erased route uses, so it also picks up boxed value classes for free.

using IPet rex = new Dog("Rex"); Assert.Same(rex, Helpers.AdoptPet<IPet>(rex)); // fun <T : Pet> adoptPet(pet: T): T Assert.Same(rex, PetRelayKt.RelayPet<IPet>(rex)); // fun <T> relayPet(value: T): T, unconstrained

Every top-level fun <T>, whatever its parameters and return, binds like a generic method on a class: a static generic method on the file's class, with every T position boxed. A builtin T (Identity<int>(4)) therefore crosses as a box, not as a single typed call. A parameter named handle keeps that name on a top-level fun <T>.

fun <T : Any> describe(value: T): String = "$value: ${value::class.simpleName}" fun <T> label(value: T): String = "bowl of $value" fun <T> pick(first: T, second: T): T = second fun <T : Any> tally(x: T, n: Int): String = "$x x$n"
Assert.Equal("4: Int", Descriptions.Describe(4)); Assert.Equal("bowl of Mylo", Descriptions.Label("Mylo")); Assert.Equal(9, Descriptions.Pick(7, 9)); Assert.Equal("4 x2", Descriptions.Tally(4, 2)); // public static string Describe<T>(T value) where T : notnull // public static string Label<T>(T value) // public static T Pick<T>(T first, T second) // public static string Tally<T>(T x, int n) where T : notnull

A top-level fun <T> with a suspend lambda or Flow in the signature (a Flow return has no top-level route) or a reified T whose bound C# cannot spell is not generated, and the warning names which one. A suspend fun <T> binds as a Task<T>; see A suspend fun with its own type parameter.

A C# builtin (int, double, string, short, ...) works as T, with a stdlib bound or none. A bound C# cannot name is dropped, so a type argument outside it compiles and throws KotlinInvalidCastException at the call:

fun <T : Number> weigh(value: T): T = value
Assert.Equal(4, Treats.Weigh(4)); Assert.Equal(2.5, Treats.Weigh(2.5)); Assert.Throws<KotlinInvalidCastException>(() => Treats.Weigh(3u)); // uint is not a Kotlin Number

Generic methods on a class

A member function's own type parameter becomes a C# generic method, with the constraint and with inference at the call site. This works on a class, a companion, an object, a generic class, an abstract class, a sealed base and a sealed arm:

class Groomer { fun <T> echo(item: T): T = item fun <T : Furry> adopt(pet: T): T = pet fun <T> nothing(): T? = null fun <T> wrap(item: T): Result<T> = Result.success(item) companion object { fun <T> first(item: T): T = item } } sealed class Chore { class Fetch(val toy: String) : Chore() { fun <T> pick(item: T): T = item } }
using var groomer = new Groomer(); Assert.Equal(4, groomer.Echo(4)); Assert.Equal("x", groomer.Echo("x")); using Tabby same = groomer.Adopt(oreo); // where T : IFurry Assert.Null(groomer.Nothing<string>()); // a T only the return mentions is explicit Assert.True(groomer.TryWrap(7, out int value, out Exception? failure)); Assert.Equal(7, Groomer.First(7)); using var fetch = new Chore.Fetch("ball"); Assert.Equal(7, fetch.Pick(7));

A suspend member binds as a generic Task<T> method on a class or sealed arm, generic owner included; see A suspend fun with its own type parameter and suspend members of a generic class.

What differs from a generic class:

  • Every T crosses as a boxed handle, so a builtin type argument (int, string, double) works and there is nothing for you to dispose. A wrapper type argument comes back as the same Kotlin object, and you dispose it as usual.

  • A bound is a where clause (T : Any is where T : notnull), and several bounds each list. A type argument outside a bound C# cannot name throws KotlinInvalidCastException before the body runs. An Enum<T> bound is where T : struct, global::System.Enum (An enum bound).

  • A Result<T> return unwraps to T and gains its Try twin, as on any other member.

  • An override repeats no where clause, which C# forbids, and an override that spells T? says where T : default. Write your own C# override the same way.

  • A type parameter named like its member or its owner (fun <Hold> hold) is renamed with a T prefix (THold).

These stay a named skip, with the specific reason in the warning: a generic method on an interface, an enum class, or a value class (an extension fun <T> X.f(), sync or suspend, binds, see Generic and bare T receivers); a reified type parameter on a member (a top-level fun <T> binds one); a bound that is itself a type parameter; a T nested in a type that is not an exported generic class or generic value class (List, Set, Map and Result aside); a multi-bound T that no parameter mentions; and a T that shadows a type parameter in scope (the owner's, or one an inner class captures). A generic method on an inner class or a type nested in a generic owner binds like any other. Expose a non-generic wrapper for any of the refused shapes. The warning ends its reason with the shape it refused:

... but not at this position (here, its type parameter `T` is reified). expose a non-generic wrapper (`fun f(value: Int)` beside `fun <T> f(value: T)`), or declare it as a class, object, ...

A type parameter as an element

A method's own T, or a generic class's T, can be the element of a List, Set or Map, in a parameter, a return or a getter. It binds on a class, object, companion, generic class member and top-level function. T is inferred from the argument.

class Deck { fun <T> shuffle(list: List<T>): List<T> = list.reversed() fun <T> pad(list: List<T?>): List<T?> = list + null fun <T : Furry> groom(pets: List<T>): List<T> = pets } fun <T> shuffle(list: List<T>): List<T> = list.reversed() // top level
IReadOnlyList<T> Shuffle<T>(IReadOnlyList<T> list) IReadOnlyList<T?> Pad<T>(IReadOnlyList<T?> list) IReadOnlyList<T> Groom<T>(IReadOnlyList<T> pets) where T : IFurry
using var deck = new Deck(); IReadOnlyList<int> ints = deck.Shuffle(new[] { 1, 2, 3 }); // 3, 2, 1 IReadOnlyList<int?> padded = deck.Pad(new int?[] { 1, null }); // 1, null, null IReadOnlyList<string> names = Descriptions.Shuffle(new List<string> { "Oreo", "Mylo" }); using var oreo = new Tabby("Oreo", 3); foreach (Tabby cat in deck.Shuffle(new[] { oreo })) cat.Dispose(); // returned elements are yours

A passed wrapper element is borrowed, and each wrapper element Kotlin returns is yours to dispose. List<List<T>> works, and a bounded T keeps its where clause. A Map key must be non-null: fun <K : Any, V> tally(map: Map<K, V>) binds, while an unconstrained K is skipped as NULLABLE_MAP_KEY. Declare K : Any. A T with an enum or self-referencing bound is a valid element (An enum bound), and so is one with several bounds that no bare parameter mentions:

class Arena<T>(val champion: T) where T : Pet, T : Trainable { fun team(pets: List<T>): Int = pets.sumOf { it.tricks } + champion.tricks }
int tricks = arena.Team(new[] { mylo, cookie }); // where T : IPet, ITrainable

A T in a lambda binds; see below.

A type parameter in a lambda

A method's own T, or a generic class's T, can be a payload or the result of a non-suspend lambda parameter. T is inferred from the arguments, and the lambda is an ordinary Action<T> or Func<T, ...>.

class Dealer { fun <T> each(items: List<T>, action: (T) -> Unit) = items.forEach(action) fun <T> map(value: T, transform: (T) -> T): T = transform(value) fun <T : Furry> rename(pet: T, pick: (T) -> T): String = pick(pet).name } class Serving<T>(val portion: T) { fun each(action: (T) -> Unit) { action(portion) } } fun <T> echoer(): (T) -> T = { item -> item } // top level
void Each<T>(IReadOnlyList<T> items, Action<T> action) T Map<T>(T value, Func<T, T> transform) string Rename<T>(T pet, Func<T, T> pick) where T : IFurry Func<T, T> Echoer<T>()
using var dealer = new Dealer(); dealer.Each(new[] { "Oreo", "Mylo" }, name => seen.Add(name)); int eight = dealer.Map(4, age => age * 2); using var oreo = new Tabby("Oreo", 3); dealer.Each(new[] { oreo }, cat => { using (cat) names.Add(cat.Name); }); // the payload is yours using Tabby same = dealer.Map(oreo, cat => cat); // the result is yours too

A wrapper payload follows the usual callback ownership: the lambda owns it and disposes it. A wrapper the lambda returns, whether the payload, one you already hold or a fresh one, needs no extra step: Kotlin takes its own reference, so you still dispose any wrapper you hold. A T? payload, or a T that is null, arrives as null. A bound with no closed spelling (T : Enum<T>) works in both directions.

A suspend lambda or a Flow parameter over T binds too. A stored callback or listener pair over T binds too, on a generic class and on a generic sealed arm (addShake(listener: (T) -> Unit) is IDisposable AddShake(Action<T> listener) on Shaker<T>; see Stored callbacks). A generic sealed arm's lambda members bind like the class's.

A type parameter inside a generic class or a Result

A method's own T can be an argument of an exported generic class (Basket<T>, Basket<T>?, Basket<Basket<T>>, List<Basket<T>>, a generic sealed Pickup<T>), and the payload of a Result return (Result<List<T>>, Result<Set<T>>, Result<Map<K, V>> with K : Any, Result<Basket<T>>). It binds on the same owners as a bare T, and T is inferred from the argument. A multi-bound T works here too: fun <T> crown(basket: Basket<T>): T where T : Furry, T : Tricky is T Crown<T>(Basket<T> basket) where T : IFurry, ITricky.

class Groomer { fun <T> basket(item: T): Basket<T> = Basket(item) fun <T> unpack(basket: Basket<T>): T = basket.item fun <T : Furry> brushBasket(basket: Basket<T>): T = basket.item fun <T> gather(items: List<T>): Result<List<T>> = Result.success(items) }
Basket<T> Basket<T>(T item) T BrushBasket<T>(Basket<T> basket) where T : IFurry IReadOnlyList<T> Gather<T>(IReadOnlyList<T> items) bool TryGather<T>(IReadOnlyList<T> items, out IReadOnlyList<T> value, out Exception? failure)
using var groomer = new Groomer(); using Basket<int> treats = groomer.Basket(3); int three = groomer.Unpack(treats); // treats is borrowed, still usable using var oreo = new Tabby("Oreo", 3); using Tabby brushed = groomer.BrushBasket(groomer.Basket(oreo)); // a bound is witnessed through the basket IReadOnlyList<string> names = groomer.Gather(new List<string> { "Oreo", "Mylo" });

A returned basket is yours to dispose, and a passed one is borrowed. A Result payload gains the Try twin of any other Result return. A Basket<T> also works inside a class-T member (Basket<Basket<T>>) and reads its inner basket without the consumer touching Basket<int> first. The same holds at any depth and any position: a getter or setter, a parameter, a list or map element, a callback argument, a Flow element, a returned lambda, a suspend result or a Result payload.

class Satchel<T>(val item: T) { val nested: Basket<Basket<T>> = Basket(Basket(item)) val baskets: List<Basket<T>> = listOf(Basket(item), Basket(item)) }
using var satchel = new Satchel<short>(7); using Basket<Basket<short>> nested = satchel.Nested; using Basket<short> inner = nested.Item; // no prior use of Basket<short> needed

What still skips, named:

  • kotlin.Array<T>.

  • A nested generic class argument the wire cannot read, such as Box<List<T>>.

Type aliases

A typealias erases to its underlying type; there is no separate alias type in the generated C#. This holds on every route, including a use-site ? on the alias itself and a suspend or Flow member, so an aliased type behaves exactly like the written-out type would, refusals included:

typealias Score = Int typealias PetName = String typealias Box<T> = List<T> fun topScore(): Score = 10 suspend fun greetLater(name: PetName?): PetName? { /* ... */ } fun boxedTallies(tallies: Box<Int>): Box<Int> = tallies.map { it * 2 }
int score = TypeAliases.TopScore(); // Score is just Int string? greeting = await TypeAliases.GreetLaterAsync(null); // PetName? is just string? Assert.Null(greeting); IReadOnlyList<int> doubled = TypeAliases.BoxedTallies([2, 3]); // Box<Int> is just List<Int>

A generic alias substitutes its type parameter only when the parameter is one of the RHS's own top-level type arguments, as Box<T> above does. A parameter nested deeper in the RHS (typealias Pages<T> = List<Map<String, T>>) is not substituted; a member using it is skipped with a named warning instead of generating a wrong binding. Write the type out at that position if you need it exported.

Erasure applies to an extension's receiver too, including a nested-type alias (see Extensions: Nested receivers).

Limitations

A generic class declared in a dependency module is exported like a module-local one once the export closure reaches it, with Int and String type arguments checked. Other type arguments on a dependency generic class are not checked; if one fails, declare the class in the publishing module itself.

A T? written at a generic function's return (fun <T> f(item: T): Crate<T?>) is spelled T in C#, so the type argument you choose decides whether null can be held.

A generic class binds its ordinary members, including a member that takes a lambda (box.Measure(n => n * 10)). Its Flow, StateFlow and MutableStateFlow members bind too, as KotlinFlow<T> and its siblings (see A Flow with a type parameter), and so do its suspend members (see suspend members of a generic class), and its lambda members, stored callback pairs and interface listener pairs, a Char payload included.

A type nested inside a generic class is declared on a non-generic static class beside it: Box<T> { class Lid } gives Box<int> and a separate Box.Lid, never Box<int>.Lid. See Classes and objects: Nested types and generic classes. A member that returns an inner class of a generic owner is skipped with a named diagnostic, but the generic owner is still exported.

Last modified: 03 November 2026