Skip to content

Type conversions

Every exported argument must implement FromNapiValue (or one of the reference conversion traits), and every returned value must implement ToNapiValue. The generated TypeScript type is useful documentation, but the Rust trait implementation is what determines whether a conversion is actually available.

This reference describes napi-rs v3's bindgen runtime. For the lower-level handles such as JsString and JsObject, see Env and low-level values.

Direction legend

Mark Meaning
JS → Rust The type can be used as an exported function argument.
Rust → JS The type can be returned or assigned to a JavaScript value.
Scoped The Rust value borrows a Node-API environment or JavaScript callback scope and must not escape it.
Owned Conversion creates or retains Rust-owned data that can outlive the callback, subject to the type's Send rules.

WARNING

A TypeScript mapping does not imply both conversion directions. For example, u64 generates bigint but is output-only; use BigInt when accepting an arbitrary JavaScript bigint so you can check whether narrowing is lossless.

Primitive values

Rust type JavaScript / TypeScript Direction Ownership and caveats Feature / minimum Node-API
() / Undefined undefined; a function return becomes void Both Zero-sized marker. Under strict, input must be undefined. Base API
Null null Both Explicit null marker. Plain input conversion accepts and discards any value; under strict, input must be null. Base API
bool boolean Both Copied. Base API
i8, u8, i16, u16, i32, u32 number Both Integer conversion; JavaScript still stores a Number. Base API
f32 number Rust → JS Widened to a JavaScript double; there is no FromNapiValue implementation. Use f64 for input. Base API
f64 number Both JavaScript Number is IEEE-754 double precision. Base API
i64 number Both Uses Node-API's signed 64-bit Number conversion. Values outside JavaScript's safe-integer range can lose precision. Base API
BigInt bigint Both Keeps a sign bit and little-endian u64 words. Its getters report whether narrowing was lossless. napi6
u64, u128, i128, usize, isize, i64n bigint Rust → JS Output-only to avoid silently narrowing arbitrary JavaScript BigInts. napi6
String string Both Owned UTF-8 string. Base API
&str string Rust → JS Borrowed Rust output only; JavaScript strings cannot be accepted as &str. Use String for input. Base API
Utf16String string Both Owned UTF-16 code units; useful when exact UTF-16 representation matters. Base API
Latin1String string Both Owns Latin-1 bytes. Formatting it as UTF-8 requires latin1. Base API; latin1 for decoding/display
OsString, PathBuf string Both Owned. Windows uses UTF-16 and preserves unpaired surrogates. Unix output rejects a non-Unicode path rather than replacing bytes. Base API
&OsStr, &Path string Rust → JS Borrowed output. Same platform caveats as the owned forms. Base API
Symbol symbol Both Plain input conversion discards the value without retaining identity or description. #[napi(strict)] first validates that it is a symbol, but still does not retain it. Returning Symbol creates one from Rust descriptor state. Use scoped JsSymbol to preserve an existing value. Symbol::for_desc needs Node-API 9. Base API; napi9 for global symbols

i64 deliberately maps to number, while i64n maps to bigint. Prefer the wrapper only when the JavaScript API is intended to expose a BigInt.

lib.rs
rust
#[napi]
pub fn inspect_bigint(value: BigInt) -> Result<u64> {
  let (negative, narrowed, lossless) = value.get_u64();
  if negative || !lossless {
    return Err(Error::from_reason("value does not fit in u64"));
  }
  Ok(narrowed)
}

The return type above is bigint because u64 is a BigInt output type.

Option, null, and undefined

Option<T> has an intentionally asymmetric mapping:

Position JavaScript accepted or produced Generated TypeScript
Function argument T, null, or undefined; both nullish values become None T | null | undefined and normally an optional trailing parameter
Function return Some(T) becomes T; None becomes null T | null
#[napi(object)] or structured-shape field with default use_nullable = false Missing or undefined becomes None; explicit null is passed to the inner T conversion and normally fails; None is omitted on output field?: T
#[napi(object)] or structured-shape field with use_nullable = true Missing or undefined is an error; null becomes None; None is emitted as null field: T | null
Public class field The accessor always exists. An Option getter emits null for None, and a writable setter accepts the normal nullish Option inputs. use_nullable changes the generated property/constructor shape, not whether the accessor exists. Default: field?: T; with use_nullable: field: T | null

Use Null or Undefined when the distinction itself is part of the API. Use Either<T, Null> or Either<T, Undefined> when exactly one nullish value is accepted.

lib.rs
rust
#[napi]
pub fn optional_name(value: Option<String>) -> Option<String> {
  value.filter(|name| !name.is_empty())
}

#[napi]
pub fn null_but_not_undefined(value: Either<String, Null>) -> bool {
  matches!(value, Either::B(Null))
}

INFO

Non-trailing optional parameters may be emitted as required unions so that a later required parameter remains callable in TypeScript. The union still accepts undefined and null.

Unions with Either

Either<A, B> through Either26<A, ..., Z> map to TypeScript unions. On input, napi-rs tests variants from left to right with each type's ValidateNapiValue implementation, then converts the first match.

lib.rs
rust
#[napi]
pub fn normalize_id(value: Either<u32, String>) -> String {
  match value {
    Either::A(number) => number.to_string(),
    Either::B(text) => text,
  }
}
index.d.ts
ts
export function normalizeId(value: number | string): string

Order overlapping alternatives from most specific to least specific. Validation for a plain Object, for example, cannot prove a complete object schema. Either is a runtime union, not a serde-style untagged enum with backtracking after arbitrary user code.

Arrays, tuples, maps, and sets

Rust type JavaScript / TypeScript Direction Conversion behavior Feature
Vec<T> Array<T> Both Copies/converts every element. Input requires each element to implement FromNapiValue. Base API
[T; N] Array<T> Rust → JS Creates a JavaScript array. Base API
Rust tuples, up to supported generated arity TypeScript tuple / JS Array Both Input must have at least the tuple's length; each indexed element is converted. Base API
Array<'env> unknown[] JS → Rust and scoped pass-through Scoped handle with get, get_ref, set, and insert; avoids converting the whole array up front. Base API
HashMap<K, V>, BTreeMap<K, V> Record<K, V> / plain object Both Uses own enumerable string-keyed properties. This is not JavaScript Map. Keys must convert to/from strings. Base API
IndexMap<K, V> Record<K, V> / plain object Both Uses the same own enumerable string-keyed property shape while preserving Rust insertion order where JavaScript's property rules allow it. object_indexmap
HashSet<T>, BTreeSet<T> Set<T> Both Constructs or iterates an actual JavaScript Set. Base API
IndexSet<T> Set<T> Both Insertion-ordered Rust set. object_indexmap

Conversion of Vec<T> and collections is O(n). Use scoped Array, Object, typed-array views, or a stream when you need incremental access rather than an owned copy.

Objects, classes, and custom shapes

Rust type or declaration JavaScript / TypeScript Direction Ownership / identity
Object<'env> object Both within scope Direct scoped handle. Property access crosses the Node-API boundary on each operation.
ObjectRef object Both Holds a Node-API reference so the object can outlive the callback.
Unknown<'env> unknown Both within scope Unchecked scoped handle; inspect/coerce it explicitly.
#[napi(object)] struct Plain object / interface Controlled by object_from_js and object_to_js, both on by default JavaScript input is converted into a new owned Rust struct. Mutating it does not mutate the source object.
#[napi] struct JavaScript class Through class references and instances Preserves native class identity. Methods receive &self/&mut self; public fields become accessors.
ClassInstance<'env, T> An instance of class T JS → Rust / scoped output Use inside object fields or collections when the JavaScript class instance itself is needed.
#[napi(transparent)] struct Wrapper(T) Same representation as T Controlled per direction Rust newtype without a JavaScript wrapper object.
#[napi(array)] tuple struct JavaScript array / TypeScript tuple Controlled per direction Named Rust type with positional JavaScript representation.
Structured #[napi] enum Discriminated object union Controlled per direction Owned conversion; discriminator defaults to type.

Do not treat a class as an object shape. A normal read-write public class field needs both output conversion for its getter and input conversion for its setter; a readonly field needs only output conversion, while a skipped class field has no accessor. For nested class values, accept &T, ClassInstance<T>, or use Array::get_ref; Vec<T> requires an owned FromNapiValue implementation and is therefore not the way to accept a list of class instances.

See Classes, Objects, Enums, and #[napi] attributes for shape-specific examples.

Buffers, ArrayBuffers, and typed arrays

Rust type JavaScript / TypeScript Direction Lifetime and data behavior Feature / minimum Node-API
BufferSlice<'env> Node.js Buffer Both within a synchronous scope Mutable borrowed view for synchronous code. Do not hold it across await. Base API
Buffer Node.js Buffer Both Keeps a reference to JavaScript-owned data and is designed for async use. Cloning references the same underlying buffer. Best lifecycle handling with napi4
ArrayBuffer<'env> ArrayBuffer JS → Rust and scoped pass-through Borrowed bytes tied to the environment. Base API
Int8Array, Uint8Array, … Corresponding typed array Both Owned/reference-retaining wrappers suitable for async use. BigInt array variants require napi6
Int8ArraySlice<'env>, Uint8ArraySlice<'env>, … Corresponding typed array Both within scope Borrowed views for synchronous code. BigInt array variants require napi6
&[i8], &[u8], &[i16], … Corresponding typed array JS → Rust in a synchronous callback Borrowed slice; cannot outlive the callback. BigInt slices require napi6

External buffers and ArrayBuffers can be zero-copy when the runtime accepts external backing stores. A runtime may reject external buffers; constructors such as BufferSlice::from_data then fall back to a copy. Do not promise zero-copy behavior across every Node-compatible runtime. See Typed arrays and Understanding lifetime.

Dates and serde JSON

Rust type JavaScript / TypeScript Direction Feature / caveat
Date (JsDate) Date Scoped low-level value napi5
chrono::DateTime<Tz>, NaiveDateTime Date Both chrono_date, which enables chrono and napi5; milliseconds since epoch determine precision.
serde_json::Value JSON-compatible JavaScript value Both serde-json; rejects functions, undefined, symbols, and external values.
serde_json::Map<String, Value> Plain object Both serde-json
serde_json::Number Number, BigInt, or string depending value and enabled API Both serde-json; with napi6, out-of-safe-range integers are emitted as BigInt.

serde_json::Value is not a lossless representation of arbitrary JavaScript. In particular, a large input BigInt may become a JSON number when it fits or a decimal string when it does not. Use BigInt when BigInt identity and exact narrowing rules matter.

serde-json-ordered additionally enables serde_json's preserve_order behavior.

Functions, promises, and streams

Rust type JavaScript / TypeScript Direction Lifetime / feature
Function<'env, Args, Return> Typed JavaScript function JS → Rust in scope; can be passed through in scope Calls JavaScript only on its owning thread. Use FnArgs<(...)> for multiple positional arguments.
FunctionRef<Args, Return> Typed JavaScript function JS → Rust / retained reference Owns a Node-API reference but does not implement ToNapiValue. To pass it back, call borrow_back(env) to obtain a scoped Function; still use it only in the owning environment/thread.
ThreadsafeFunction<...> Typed callback JS → Rust, then callable from other threads napi4; see ThreadsafeFunction.
Promise<T> Promise<T> JS → Rust only Awaitable Rust future. Requires the async runtime for normal exported async use.
PromiseRaw<'env, T> Promise<T> Scoped JS promise handle Supports then, catch, and finally without moving the promise to another thread.
Rust async fn return Promise<T> Rust → JS async or tokio_rt; Result::Err rejects.
AsyncTask<T> Promise<T::JsValue> Rust → JS Runs compute on libuv's worker pool.
ReadableStream<'env, T> Web ReadableStream<T> Both web_stream; construction needs T: Send + 'static and a Send + 'static Rust stream.
WriteableStream<'env> Web WritableStream JS → Rust and scoped pass-through web_stream; the Rust API is currently spelled WriteableStream, and the type generator does not canonicalize that spelling, so use ts_arg_type = "WritableStream" for a public parameter.

ReadableStream::new checks that the runtime provides a global ReadableStream. Node-API 4 alone does not guarantee the Web Streams global; with_readable_stream_class accepts a compatible constructor explicitly.

External native data

External<T> exposes an opaque, type-tagged native allocation to JavaScript. It is not serialized and its generated type is ExternalObject<T>.

  • Return an owned External<T> to transfer it into a JavaScript external value.
  • Accept &External<T> or &mut External<T> to borrow and type-check the wrapped value.
  • Use ExternalRef<T> when Rust must keep a JavaScript reference to the external.
  • External::new_with_size_hint reports native allocation size to the JavaScript garbage collector; the number is a GC accounting hint, not a memory limit.

See External for lifecycle details.

Validation is not coercion

Most generated functions convert according to their FromNapiValue implementation. Adding #[napi(strict)] first invokes ValidateNapiValue and rejects a mismatched top-level JavaScript type. It does not coerce strings to numbers, and it does not recursively validate every property before conversion.

#[napi(return_if_invalid)] performs the same validation but returns undefined on invalid input instead of throwing. See #[napi] attributes for its constraints.

Choosing an ownership model

Use this order of preference:

  1. Use owned Rust values (String, Vec<T>, #[napi(object)]) when a copy is acceptable and the value must cross threads or await points.
  2. Use scoped handles and slices (Object<'env>, Array<'env>, BufferSlice<'env>, typed-array slices) for synchronous, zero- or low-copy access.
  3. Use reference-retaining wrappers (Buffer, ObjectRef, FunctionRef, Reference<T>) when JavaScript-owned data must outlive the callback.
  4. Use ThreadsafeFunction to invoke JavaScript from another thread; never move a scoped JavaScript handle there.
  5. Use a stream when data should be produced incrementally instead of copied into one collection.

The compiler enforces many of these boundaries through lifetimes and Send, but an unsafe method or raw Node-API handle can bypass them. Read Understanding lifetime before doing so.