Generics, Macros, and Type Erasure
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.