---
title: 'TypedArray'
description: JavaScript TypedArray primitive.
---

# TypedArray

`TypedArray` describes an array-like view of an underlying [binary data buffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). NAPI-RS can expose a view of that storage to Rust without copying it, subject to the lifetime and synchronization rules below.

## Buffer

[`Buffer`](https://nodejs.org/api/buffer.html) is a
subclass of JavaScript's
[`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array).
It is often used to share data between Node.js and Rust.

A `Buffer` can be created from `Vec<u8>`. Where the runtime permits external
buffers, NAPI-RS transfers the allocation to the JavaScript `Buffer` without a
copy, and its finalizer releases the `Vec<u8>` after JavaScript collects the
buffer. If the runtime rejects external buffers, NAPI-RS falls back to copying
the bytes into a runtime-owned buffer.

**lib.rs**

```rust {6}
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn create_buffer() -> Buffer {
  vec![0, 1, 2].into()
}
```

::: info
On runtimes that support external buffers, the underlying `Vec<u8>` is not
copied in this path.

:::

::: warning
The `Electron` will not be able to create `Buffer` in zero copy way. See [V8
Memory Cage](https://www.electronjs.org/blog/v8-memory-cage) for more details.
**NAPI-RS** will copy the data of the `Vec<u8>` into the underlying `Buffer` in this case.

:::

## Buffer and TypedArray Types

**NAPI-RS** provides two categories of buffer types for different use cases. For more details on how lifetimes work for these types, see [Understanding Lifetime](/docs/concepts/understanding-lifetime#lifetime-of-buffer-and-typedarray).

### Owned Types

These types can outlive the current native call and cross async boundaries:

- `Buffer` - Reference-backed Node.js Buffer wrapper
- `Uint8Array`, `Int32Array`, `Float64Array`, etc. - Owned typed-array wrappers

For a value received from JavaScript, NAPI-RS creates a [`napi_ref`](https://nodejs.org/api/n-api.html#napi_create_reference). It keeps the JavaScript object and backing store alive until the Rust wrapper is dropped. Dropping the wrapper only releases Rust's reference; JavaScript may retain the same object independently.

**lib.rs**

```rust {5}
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn process_buffer(env: &Env, buffer: Buffer) -> Result<AsyncBlock<Buffer>> {
  // Copy while the synchronous JavaScript callback still has control.
  let mut data = buffer.to_vec();
  AsyncBlockBuilder::new(async move {
    data.reverse();
    Ok(data.into())
  })
  .build(env)
}
```

::: info
`AsyncBlock` and `AsyncBlockBuilder` are re-exported under napi's `async`
feature, so this example does not compile without it. Enable the feature on the
`napi` dependency in your `Cargo.toml`:
`napi = { version = "3", features = ["async"] }`. The `tokio_time` feature is
only required for the `napi::tokio::time::sleep` helper shown later.

:::

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

**index.d.ts**

```ts
export declare function processBuffer(buffer: Buffer): Promise<Buffer>
```

::: warning
`Buffer` and the owned typed-array wrappers implement `Send` and `Sync` so
their lifetime and cleanup can cross threads. Those traits do not synchronize
the shared bytes. JavaScript can retain and mutate the same backing store
while Rust holds a wrapper. Accessing it on a Rust worker while JavaScript or
another Rust thread may mutate it is a data race and can cause undefined
behavior—even if Rust only reads. Copy the bytes before dispatching work, as
above, or enforce an ownership protocol that rules out unsynchronized access.

:::

### Borrowed Types (`BufferSlice`, `Uint8ArraySlice`, etc.)

These types borrow data and are lifetime-bound to the function scope:

- `BufferSlice<'env>` - Zero-copy Buffer slice
- `Uint8ArraySlice<'env>`, `Int32ArraySlice<'env>`, etc. - Zero-copy TypedArray slices
- `ArrayBuffer<'env>` - Zero-copy ArrayBuffer view
- `&[u8]/&[i8]/&[f32]/&[f64]...` - Zero-copy slice

**lib.rs**

```rust {4}
use napi_derive::napi;

#[napi]
pub fn sum_array_slice(input: &[u32]) -> u32 {
  // Zero-copy access to the underlying data
  input.iter().sum()
}
```

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

**index.d.ts**

```ts
export declare function sumArraySlice(input: Uint32Array): number
```

**index.ts**

```ts {5}
import { sumArraySlice } from './index.js'

const input = new Uint32Array([1, 2, 3, 4, 5])

const result = sumArraySlice(input)
console.log(result) // 15
```

### When to Use Each Type

**Use `&[u8]/&[i8]/&[f32]/&[f64]...` when**:

- You need zero-copy performance
- Working in synchronous context only
- Data lifetime is bounded to the function call

**Use `BufferSlice<'env>` or `Uint8ArraySlice<'env>/Int32ArraySlice<'env>/...` when**:

- You need zero-copy performance
- You need to convert them into owned types in some scenarios
- You need to convert them into `Object` or `Unknown`

**Use `Buffer` when**:

- You need to store the buffer beyond the function call
- Working with async functions

## Common Usage Patterns

### Converting Between Types

**lib.rs**

```rust {7,10}
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn buffer_slice_to_buffer(env: &Env, slice: BufferSlice) -> Result<AsyncBlock<u8>> {
  // Convert BufferSlice to owned Buffer for async usage
  let buffer = slice.into_buffer(env)?;
  // Copy before the async work can run concurrently with JavaScript.
  let data = buffer.to_vec();
  AsyncBlockBuilder::new(async move {
    Ok(data.iter().sum())
  })
  .build(env)
}
```

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

**index.d.ts**

```ts
export declare function bufferSliceToBuffer(slice: Buffer): Promise<number>
```

**index.ts**

```ts {5}
import { bufferSliceToBuffer } from './index.js'

const slice = Buffer.from([1, 2, 3, 4, 5])

const result = await bufferSliceToBuffer(slice)
console.log(result) // 15
```

### Async vs Sync Patterns

**lib.rs**

```rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

// ✅ Correct: Using owned Buffer in async context
#[napi]
pub async fn process_async(buffer: Buffer) -> Result<Buffer> {
    // Buffer can cross await boundaries
    napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    Ok(buffer)
}

// ❌ Won't compile: BufferSlice cannot cross await boundaries
// #[napi]
// pub async fn process_async_slice(slice: BufferSlice<'_>) -> Result<BufferSlice<'_>> {
//     napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
//     Ok(slice) // Error: slice doesn't live long enough
// }

#[napi]
// ✅ Correct: Convert slice to owned for async usage
pub fn process_slice_async(env: &Env, slice: BufferSlice<'_>) -> Result<AsyncBlock<Buffer>> {
  let buffer = slice.into_buffer(env)?;
  AsyncBlockBuilder::new(async move { Ok(buffer) }).build(env)
}
```

All of the `AsyncBlock` examples above build their futures with the `napi`
crate's async support, which is gated behind the `async` feature on the `napi`
dependency (`napi = { version = "3", features = ["async"] }`). That feature is
what re-exports `AsyncBlock`/`AsyncBlockBuilder` and the Tokio runtime. The
`napi::tokio::time::sleep` helper used above additionally requires the
`tokio_time` feature.

## Memory Management

### Copied Buffers

In some cases, you cannot transfer ownership of the data to a `Buffer` or typed
array. Use `copy_from` to create a copy instead.

::: warning
If you create the `Buffer` or `TypedArray` in this way, the ownership of the
data will not be transferred to the `Buffer` or `TypedArray`, but the
underlying data will be copied, there should be performance overhead of the
data copy.

:::

**lib.rs**

```rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn create_copied_buffer(env: &Env) -> Result<BufferSlice<'_>> {
  let data = b"Hello, World!";
  BufferSlice::copy_from(env, data)
}
```

### External Buffers

Sometimes, you may want to create a `Buffer` or `TypedArray` from data types that can `deref` to `[u8]` or get the raw pointer like `*mut u8`. And you don't want to copy the whole data into a `Vec<u8>` which can be very expensive. We provide the `from_external` method to achieve this, but it's unsafe and you must ensure the data is valid until the `finalize` callback is called.

::: info
The `finalize_hint` parameter is passed to the finalizer. In the first example
below, the boxed slice is both the allocation owner and the hint, so it stays
alive until the callback receives and drops it. If the runtime rejects
external buffers, NAPI-RS first copies the bytes and then invokes that
callback immediately during `from_external`; otherwise the callback runs
when JavaScript finalizes the external buffer. Do not require the callback
to be deferred until garbage collection.

:::

**lib.rs**

```rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn create_shared_buffer(env: &Env) -> Result<BufferSlice<'_>> {
  let mut data = vec![1, 2, 3, 4, 5].into_boxed_slice();
  let data_ptr = data.as_mut_ptr();
  let len = data.len();

  unsafe {
    BufferSlice::from_external(env, data_ptr, len, data, move |_, boxed_data| {
      drop(boxed_data);
    })
  }
}

#[napi]
pub fn create_external_buffer(env: &Env) -> Result<BufferSlice<'_>> {
  let mut data = vec![1, 2, 3, 4, 5];
  let data_ptr = data.as_mut_ptr();
  let len = data.len();
  let capacity = data.capacity();

  // make sure the data is valid until the finalize callback is called
  std::mem::forget(data);

  unsafe {
    BufferSlice::from_external(env, data_ptr, len, data_ptr, move |_, ptr| {
      // Cleanup data when JavaScript GC runs
      std::mem::drop(Vec::from_raw_parts(ptr, len, capacity));
    })
  }
}
```

## Safety Considerations

### External Buffer Safety

When using `from_external` methods, ensure:

1. **Pointer Validity**: The pointer must remain valid until the finalize callback
2. **Memory Layout**: The memory must be compatible with the expected type
3. **Proper Cleanup**: The finalize callback must properly deallocate memory

**lib.rs**

```rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn unsafe_external_example(env: &Env) -> Result<BufferSlice<'_>> {
  let mut data = vec![1u8, 2, 3, 4, 5];
  let ptr = data.as_mut_ptr();
  let len = data.len();
  let capacity = data.capacity();

  // ⚠️ CRITICAL: Must forget the Vec to prevent double-free
  std::mem::forget(data);

  unsafe {
    BufferSlice::from_external(env, ptr, len, ptr, move |_, ptr| {
      // ✅ Properly reconstruct and drop the Vec
      std::mem::drop(Vec::from_raw_parts(ptr, len, capacity));
      // Vec automatically deallocates when dropped
    })
  }
}
```

### Unsafe mutable access

The unsafe `as_mut` methods expose a mutable slice into storage that JavaScript
may also access. Calling the method is only sound when you can guarantee that
JavaScript and every other Rust alias will neither read nor write the backing
store for the entire mutable borrow. Violating that contract can cause
undefined behavior. In cross-thread code, prefer an owned copy unless you have
an explicit synchronization and ownership protocol spanning both JavaScript
and Rust.
