Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generics, Macros, and Type Erasure

Type designStatic inside, erased at collectionsRevision 89f33cb

Deno uses generics where concrete types improve implementation safety, then erases them where heterogeneous runtime collections require one representation.

Ops recover static types at a dynamic boundary

JavaScript supplies dynamically typed values. An #[op2] declaration describes the Rust contract with String, Option<ResourceId>, borrowed buffers, serde values, and Result<R, E>. Generated glue performs conversion and maps errors.

This keeps repetitive V8 conversion machinery out of each implementation while leaving the native body ordinary typed Rust.

The op driver is generic over result mapping

OpDriver<C: OpMappingContext> is generic over the environment into which an op result is mapped. Production uses V8OpMappingContext; tests can use a simpler context without constructing a full V8 runtime.

Its submission methods remain generic over each future’s output and error:

fn submit_op_fallible<R, E, const LAZY: bool, const DEFERRED: bool>(
    op: impl Future<Output = Result<R, E>>,
    rv_map: C::MappingFn<R>,
)

The concrete mapping function travels with the future. At the shared pending-op collection, the implementation erases that future and mapping information into a uniform allocation. Completion later restores the correct operation.

Resource lookup combines trait objects and generics

The table stores:

BTreeMap<ResourceId, Rc<dyn Resource>>

Insertion and retrieval are generic:

add<T: Resource>(value: T) -> ResourceId
get<T: Resource>(rid: ResourceId) -> Result<Rc<T>, ResourceError>

The trait object answers “how can unlike resources share one map?” The generic method answers “which concrete resource does this op require?” This is a strong example of using both forms of polymorphism rather than treating them as rivals.

Extension composition hides implementation families

Runtime assembly combines many extensions, ops, state initializers, and JavaScript modules. Generic builders and trait bounds preserve concrete service types while assembling them; erased op declarations give the runtime one list it can register with V8.

Public simplicity versus internal complexity

The caller sees:

const conn = await Deno.connect({ hostname, port });

Internally, that crosses generated conversion, permission policy, configurable services, error classification, async future erasure, promise correlation, and resource type erasure. The complexity belongs behind the boundary because it exists to preserve a simple, safe capability.

Use this heuristic in your own APIs:

  • Keep concrete types and generics along a single compile-time composition path.
  • Introduce a trait object where unlike values must share storage or runtime dispatch.
  • Erase only what the shared boundary requires; preserve typed results for the caller whenever possible.