---
url: /strict-typing/generics.md
description: Write functions and aliases that stay precisely typed for every caller.
---

# Generics

*Written: {{ $frontmatter.date }}*

## Learning objectives

* You write generic functions that remember member types.
* You let the checker infer type args and instantiate openly only when needed.
* You build generic aliases with defaults.
* You name function-type args for readers and tools.
* You constrain with unions and intersections.
* You ship typed wrappers and skip generics that add noise.

## Prerequisites

* [Annotations and Inference](./annotations-and-inference.md)
* [Casts and Assertions](./casts-and-assertions.md)
* [Functions Advanced](../luau-basics/functions-advanced.md)

## Generic functions remember what any forgets

A generic function takes many input types but keeps each call precise. The checker remembers the type you passed.

The precise rule is a type param on the function. You write `local function first<T>(list: {T}): T?` and `T` stands for the caller type. The same idea powers list and map aliases like `List<T>` and `Map<K, V>` in [type checking](https://create.roblox.com/docs/en-us/luau/type-checking). The dedicated guide defines generic functions and shows inference at calls in [generics](https://luau.org/types/generics/).

Each extra param must earn its keep. One unused `T` confuses readers and weakens errors.

One `first`, one `retry`, one `Result` serves numbers, strings, and records. Callers keep full type info without casts.

Put this Script in ServerScriptService.

```luau
--!strict
local function first<T>(list: { T }): T?
	if #list == 0 then
		return nil
	end
	return list[1]
end
local a = first({ 10, 20 })
local b = first({ "Mara", "Quill" })
```

This block checks clean. You infer `a` as `number?` and `b` as `string?`. The checker infers `T` as `number` on the first call and `string` on the second. No annotation at the call is needed. An empty list returns nil through the optional.

## Inferred calls stay clean, explicit calls pin intent

You let inference pick types. You pin them only when inference picks wrong or too wide.

The precise rule is inference first, `<<>>` when needed. A call like `first({1, 2})` infers `T`. A call like `pick<<number?>>(nil)` pins `T` to `number?`. Explicit instantiation with `<<>>` is defined in [explicit instantiation](https://rfcs.luau.org/explicit-type-parameter-instantiation.html) and summarized in [generics](https://luau.org/types/generics/).

Explicit args repeat what readers already see. Overuse hides the values behind type noise. Some calls cannot infer, like a bare `nil` with no context, so you must pin or annotate the receiver.

You write plain calls by default. You add `<<>>` for nil seeds, supertype intent, or ambiguous empties.

Put this Script in ServerScriptService.

```luau
--!strict
local function wrap<T>(value: T): { value: T }
	return { value = value }
end
local inferred = wrap(5)
local pinned = wrap<<number?>>(nil)
```

This block checks clean. You infer `inferred` as `{ value: number }` and `pinned` as `{ value: number? }`. The first call infers `T` as `number`. The second pins `T` to `number?` so nil is accepted as the wrapped empty. Without pinning, a bare nil call would infer too narrowly.

Put this Script in ServerScriptService.

```luau
--!strict
local function wrap<T>(value: T): { value: T }
	return { value = value }
end
local multi = wrap<<string, number>>( "hi" )
```

This block errors. `wrap` takes one type param but the call supplies two. You fix it by passing one arg or by defining a two-param generic. The error proves explicit lists must match the declared list.

## Generic aliases name containers, defaults shorten use

Lists, maps, results, and options repeat. An alias names the pattern once.

The precise rule is params plus optional defaults. You write `type List<T> = { T }` and `type Map<K, V> = { [K]: V }` as in [type checking](https://create.roblox.com/docs/en-us/luau/type-checking). You add defaults like `type Box<T = string> = { value: T }` so `Box` means `Box<string>`. Defaults for aliases are defined in [default type params](https://rfcs.luau.org/syntax-default-type-alias-type-parameters.html). Generic functions cannot take defaults [generics](https://luau.org/types/generics/).

Once one alias param has a default, later params need defaults too. A later param can reuse an earlier one, like `type Eq<T, U = T>`. You cannot skip this order.

Common cases stay short. Rare cases pass explicit args. Refactors add params with defaults instead of editing every use.

Put this ModuleScript in ReplicatedStorage.

```luau
--!strict
export type Box<T = string> = { value: T }
export type Result<T, E = string> =
	{ ok: true, value: T }
	| { ok: false, err: E }
local short: Box = { value = "gem" }
local num: Box<number> = { value = 4 }
```

This block checks clean. You infer `short` as `Box<string>` and `num` as `Box<number>`. The default fills `string` when you omit the arg. An explicit arg overrides it. Both records keep their member types.

## Named args document function values

A function type like `(number, string) -> string` hides which is which. Names reveal intent.

The precise rule is optional names in function types. You write `type Greet = (name: string, zone: string) -> string`. Names do not change checking. They improve hover text and completions. Named function args are defined in [named function args](https://rfcs.luau.org/syntax-named-function-type-args.html). The [syntax tour](https://luau.org/syntax/) shows function types in the same family.

Two function types with different arg names but same types still match. Renaming never fixes a type error. It only helps readers.

Callbacks, retry hooks, and event handlers state each input. Reviewers stop guessing order.

Put this Script in ServerScriptService.

```luau
--!strict
export type Award = (name: string, count: number) -> string
local function run(award: Award): string
	return award("Mara", 2)
end
local got = run(function(name, count)
	return name .. tostring(count)
end)
```

This block checks clean. You infer `got` as `string`. The alias names both inputs. The anonymous handler matches by type. Hovering `award` shows the names.

## Unions and intersections constrain without new syntax

Sometimes a generic must carry a field or accept only a few shapes. You state that bound with composition.

The precise rule is constrain by use. Luau has no `where` bound on generics in these docs. You intersect to require shape, like `T & { name: string }`. You union to allow choices, like `T | string`. Unions and intersections are defined in [type checking](https://create.roblox.com/docs/en-us/luau/type-checking) and the [syntax tour](https://luau.org/syntax/).

A failed intersection reports the missing member, not a clean bound message. A wide union accepts more than you meant when members overlap.

You require `name` by intersecting a record. You accept a few literals by unioning them. You keep the generic itself reusable.

Put this Script in ServerScriptService.

```luau
--!strict
local function nameOf<T>(item: T & { name: string }): string
	return item.name
end
local got = nameOf({ name = "Mara", level = 4 })
```

This block checks clean. You infer `got` as `string`. The intersection requires `name` while keeping extra fields. A record without `name` errors at the call. That missing-field report is the bound working.

Put this Script in ServerScriptService.

```luau
--!strict
local function id<T>(value: T | string): string
	if typeof(value) == "string" then
		return value
	end
	return "other"
end
local got = id(7)
```

This block checks clean. You infer `got` as `string`. The union allows any `T` plus strings. The branch refines the string case. You use this shape when one member needs special handling.

## Typed wrappers pay rent, noise does not

A retry helper, a result type, or a once-guard repeats across features. Generics make one version serve all.

The precise rule is wrap once with params. You define `Result<T, E>` for fallible work. You define `retry<T>(work: () -> T, tries: number): T` for repeated work. Callers keep exact success types. Failures carry typed errors.

A generic wrapper around one call site adds a jump without reuse. A generic with a single use and a concrete type is just a longer spelling.

You generify shared helpers and shared records. You leave single-use code concrete. You delete a type param when every call passes the same arg.

Put this ModuleScript in ReplicatedStorage.

```luau
--!strict
export type Result<T, E = string> =
	{ ok: true, value: T }
	| { ok: false, err: E }
local function retry<T>(work: () -> T, tries: number): T
	local out = work()
	local n = 1
	while n < tries do
		n += 1
		out = work()
	end
	return out
end
local got = retry(function()
	return 7
end, 3)
```

This block checks clean. You infer `got` as `number`. `retry` runs the thunk and returns its exact type. Swap the thunk to return a string and `got` becomes `string` with no wrapper change.

## Common mistakes

Wrong: you annotate the generic away with any.

Put this Script in ServerScriptService.

```luau
--!strict
local function first(list: any): any
	return list[1]
end
local got = first({ 1, 2 })
```

This block checks clean but infers `got` as `any`. Later misuse passes silently. You fix it with a type param.

```luau
--!strict
local function first<T>(list: { T }): T?
	if #list == 0 then
		return nil
	end
	return list[1]
end
local got = first({ 1, 2 })
```

This fix checks clean. You infer `got` as `number?`. The param preserves the member type.

Wrong: you give a generic function a default.

Put this Script in ServerScriptService.

```luau
--!strict
local function wrap<T>(value: T): { value: T }
	return { value = value }
end
```

This block checks clean as written. Add `= string` to `T` and it becomes invalid because function generics take no defaults in [generics](https://luau.org/types/generics/). You fix reuse with an alias default instead.

```luau
--!strict
export type Box<T = string> = { value: T }
local one: Box = { value = "hi" }
```

This fix checks clean. You infer `one` as `Box<string>`. Defaults live on aliases, not on functions.

Wrong: you generify a single use.

Put this Script in ServerScriptService.

```luau
--!strict
local function load<T>(id: T): T
	return id
end
local got = load("player_1")
```

This block checks clean but the generic adds nothing over `string`. You fix it with a concrete signature.

```luau
--!strict
local function load(id: string): string
	return id
end
local got = load("player_1")
```

This fix checks clean. You infer `got` as `string`. Readers see the real contract at once.

## Exercises

1. Write a generic first-item helper and call it with two member types.
2. Pin a nil seed with explicit instantiation.
3. Define an alias with a default and use it short and long.
4. Name the args of a callback type.
5. Build a Result-based loader with a typed retry.

::: details Solution 1
Put this Script in ServerScriptService.

```luau
--!strict
local function first<T>(list: { T }): T?
	if #list == 0 then
		return nil
	end
	return list[1]
end
local a = first({ 3, 4 })
local b = first({ "a", "b" })
```

This block checks clean. You infer `a` as `number?` and `b` as `string?`. One body serves both lists without casts.
:::

::: details Solution 2
Put this Script in ServerScriptService.

```luau
--!strict
local function wrap<T>(value: T): { value: T }
	return { value = value }
end
local seeded = wrap<<string?>>(nil)
```

This block checks clean. You infer `seeded` as `{ value: string? }`. Explicit instantiation pins the empty to the intended member type.
:::

::: details Solution 3
Put this Script in ServerScriptService.

```luau
--!strict
export type Chest<T = number> = { loot: T }
local a: Chest = { loot = 5 }
local b: Chest<string> = { loot = "map" }
```

This block checks clean. You infer `a` as `Chest<number>`. The default keeps the common case short. The explicit arg covers the rare case.
:::

::: details Solution 4
Put this Script in ServerScriptService.

```luau
--!strict
export type Visit = (player: Player, zone: string) -> ()
local function tour(visit: Visit, guest: Player)
	visit(guest, "Dunes")
end
```

This block checks clean. Names show which arg is who. Checking still compares only the types, not the names.
:::

::: details Solution 5
Put this ModuleScript in ReplicatedStorage.

```luau
--!strict
export type Result<T, E = string> =
	{ ok: true, value: T }
	| { ok: false, err: E }
local function retry<T>(work: () -> T, tries: number): T
	local out = work()
	local n = 1
	while n < tries do
		n += 1
		out = work()
	end
	return out
end
local gems = retry(function()
	return 9
end, 2)
```

This block checks clean. You infer `gems` as `number`. The wrapper stays generic while each call stays precise.
:::

## Beyond generics: type functions and native codegen

Two senior tools sit past this chapter. **User-defined type functions** (`type function`) compute types at check time for library-grade APIs; start from [Type Functions](../rfcs/type-functions.md) and keep them small, pure, and debug-printed, a failing type function blocks the whole module's checking. **Native codegen** (`--!native` / `@native`, non-recursive) compiles hot server code to machine code per [native code generation](https://create.roblox.com/docs/luau/native-code-gen); apply it only to measured hot functions, never to whole codebases, and re-profile after enabling since codegen changes inlining and compilation cost.

## Summary

* Generic functions remember member types across calls.
* You infer by default and pin with `<<>>` for nil or supertype intent.
* Generic aliases take defaults. Generic functions do not.
* Named function args aid readers without changing checks.
* You constrain with unions and intersections.
* You generify shared wrappers and leave single uses concrete.
* Type functions compute types; native codegen compiles measured-hot code only.
* Next: [Subtyping and Compatibility](./subtyping-and-compatibility.md).
