Skip to content

Class

TIP

There is no concept of a class in Rust. We use struct to represent a JavaScript Class.

Choose the right JavaScript shape

#[napi] on a struct creates a JavaScript class with native identity and methods. Other struct attributes create value shapes instead:

Rust declaration JavaScript representation Use it for
#[napi] struct Class instance backed by one Rust value Stateful native objects, methods, identity, and references
#[napi(object)] struct Plain object copied to/from an owned Rust struct Records, options, and configuration shapes
#[napi(transparent)] struct Wrapper(T) The inner value T Rust newtypes that should not add a JavaScript wrapper
#[napi(array)] tuple struct JavaScript Array / TypeScript tuple Fixed positional data

See Type conversions for direction and ownership rules and #[napi] attributes for the complete shape controls.

Constructor

Default constructor

If all fields in a Rust struct are pub, then you can use #[napi(constructor)] to make the struct have a default constructor.

lib.rs
rust
#[napi(constructor)]
pub struct AnimalWithDefaultConstructor {
  pub name: String,
  pub kind: u32,
}
index.d.ts
ts
export class AnimalWithDefaultConstructor {
  name: string
  kind: number
  constructor(name: string, kind: number)
}

Every public field is part of the JavaScript API: napi-rs generates a getter and, unless the field is #[napi(readonly)], a setter. Its Rust type must therefore support the generated JavaScript conversion direction. Keep native-only state private, as count is in the custom-constructor example below.

Custom constructor

If you want to define a custom constructor, you can use #[napi(constructor)] on your constructor fn in the struct impl block.

lib.rs
rust
#[napi(js_name = "QueryEngine")]
pub struct JsQueryEngine {
  count: u32,
}

#[napi]
impl JsQueryEngine {
  #[napi(constructor)]
  pub fn new() -> Self {
    JsQueryEngine { count: 0 }
  }
}
index.d.ts
ts
export class QueryEngine {
  constructor()
}

WARNING

NAPI-RS does not currently support private constructor. Your custom constructor must be pub in Rust.

Factory

Besides constructor, you can also define factory methods on Class by using #[napi(factory)].

lib.rs
rust
#[napi(js_name = "QueryEngine")]
pub struct JsQueryEngine {
  count: u32,
}

#[napi]
impl JsQueryEngine {
  #[napi(factory)]
  pub fn with_initial_count(count: u32) -> Self {
    JsQueryEngine { count }
  }
}
index.d.ts
ts
export class QueryEngine {
  static withInitialCount(count: number): QueryEngine
  constructor()
}

WARNING

If no #[napi(constructor)] is defined in the struct, and you attempt to create an instance (new) of the Class in JavaScript, an error will be thrown.

test.mjs
js
import { QueryEngine } from './index.js'

new QueryEngine() // Error: Class contains no `constructor`, cannot create it!

class method

You can define a JavaScript class method with #[napi] on a struct method in Rust.

lib.rs
rust
#[napi(js_name = "QueryEngine")]
pub struct JsQueryEngine {
  count: u32,
}

#[napi]
impl JsQueryEngine {
  #[napi(factory)]
  pub fn with_initial_count(count: u32) -> Self {
    JsQueryEngine { count }
  }

  /// Class method
  #[napi]
  pub async fn query(&self, query: String) -> napi::Result<String> {
    Ok(format!("{query}: {}", self.count))
  }

  #[napi]
  pub fn status(&self) -> napi::Result<u32> {
    Ok(self.count)
  }
}
index.d.ts
ts
export class QueryEngine {
  static withInitialCount(count: number): QueryEngine
  constructor()
  query(query: string): Promise<string>
  status(): number
}

WARNING

async fn needs the napi4 and tokio_rt features to be enabled.

TIP

Any fn in Rust that returns Result<T> will be treated as T in JavaScript/TypeScript. If the Result<T> is Err, a JavaScript Error will be thrown.

Getter

Define JavaScript class getter using #[napi(getter)]. The Rust fn must be a struct method, not an associated function.

lib.rs
rust
#[napi(js_name = "QueryEngine")]
pub struct JsQueryEngine {
  count: u32,
}

#[napi]
impl JsQueryEngine {
  #[napi(factory)]
  pub fn with_initial_count(count: u32) -> Self {
    JsQueryEngine { count }
  }

  /// Class method
  #[napi]
  pub async fn query(&self, query: String) -> napi::Result<String> {
    Ok(format!("{query}: {}", self.count))
  }

  #[napi(getter)]
  pub fn status(&self) -> napi::Result<u32> {
    Ok(self.count)
  }
}
index.d.ts
ts
export class QueryEngine {
  static withInitialCount(count: number): QueryEngine
  constructor()
  get status(): number
}

Setter

Define JavaScript class setter using #[napi(setter)]. The Rust fn must be a struct method, not an associated function.

lib.rs
rust
#[napi(js_name = "QueryEngine")]
pub struct JsQueryEngine {
  count: u32,
}

#[napi]
impl JsQueryEngine {
  #[napi(factory)]
  pub fn with_initial_count(count: u32) -> Self {
    JsQueryEngine { count }
  }

  /// Class method
  #[napi]
  pub async fn query(&self, query: String) -> napi::Result<String> {
    Ok(format!("{query}: {}", self.count))
  }

  #[napi(getter)]
  pub fn status(&self) -> napi::Result<u32> {
    Ok(self.count)
  }

  #[napi(setter)]
  pub fn count(&mut self, count: u32) {
    self.count = count;
  }
}
index.d.ts
ts
export class QueryEngine {
  static withInitialCount(count: number): QueryEngine
  constructor()
  get status(): number
  set count(count: number) 
}

Class as argument

Class is different from Object. The Rust value is wrapped by a JavaScript instance and managed by that environment's garbage collector. Pass the instance back to Rust as &T for shared access or &mut T for mutable access; the value is not cloned from a plain object.

Only public struct fields become JavaScript properties. They are writable by default because napi-rs generates both accessors; #[napi(readonly)] suppresses the setter, and #[napi(skip)] suppresses both accessors. Private fields remain native implementation details. A writable field needs both ToNapiValue and FromNapiValue; a readonly field needs only ToNapiValue. See the field attribute reference, including the #[napi(constructor)] shorthand limitation.

lib.rs
rust
#[napi] 
pub fn accept_class(engine: &QueryEngine) {
  // ...
}
#[napi]
pub fn accept_class_mut(engine: &mut QueryEngine) {
  // ...
}
index.d.ts
ts
export function acceptClass(engine: QueryEngine): void
export function acceptClassMut(engine: QueryEngine): void

For nested class instances, arrays of class instances, and ClassInstance<T>, see the class section of the conversion reference.

Property attributes

The default Property attributes are writable = true, enumerable = true and configurable = true. You can control the Property attributes over the #[napi] macro:

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

// A complex struct that cannot be exposed to JavaScript directly.
#[napi]
pub struct QueryEngine {
  num: i32,
}

#[napi]
impl QueryEngine {
  #[napi(constructor)]
  pub fn new() -> Result<Self> {
    Ok(Self {
      num: 42,
    })
  }

  // writable / enumerable / configurable
  #[napi(writable = false)] 
  pub fn get_num(&self) -> i32 {
    self.num
  }
}

In this case, the getNum method of QueryEngine is not writable:

main.mjs
js
import { QueryEngine } from './index.js'

const qe = new QueryEngine()
qe.getNum = function () {} // TypeError: Cannot assign to read only property 'getNum' of object '#<QueryEngine>'

Custom Finalize logic

NAPI-RS will drop the Rust struct wrapped in the JavaScript object when the JavaScript object is garbage collected. You can also specify custom finalize logic for the Rust struct.

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi(custom_finalize)]
pub struct CustomFinalize {
  width: u32,
  height: u32,
  inner: Vec<u8>,
}

#[napi]
impl CustomFinalize {
  #[napi(constructor)]
  pub fn new(mut env: Env, width: u32, height: u32) -> Result<Self> {
    let inner_size = u64::from(width)
      .checked_mul(u64::from(height))
      .and_then(|pixels| pixels.checked_mul(4))
      .and_then(|bytes| usize::try_from(bytes).ok())
      .ok_or_else(|| Error::new(Status::InvalidArg, "image dimensions are too large"))?;
    let external_size = i64::try_from(inner_size)
      .map_err(|_| Error::new(Status::InvalidArg, "image dimensions are too large"))?;

    let mut inner = Vec::new();
    inner.try_reserve_exact(inner_size).map_err(|err| {
      Error::new(
        Status::GenericFailure,
        format!("failed to allocate image buffer: {err}"),
      )
    })?;
    inner.resize(inner_size, 0);
    env.adjust_external_memory(external_size)?;
    Ok(Self {
      width,
      height,
      inner,
    })
  }
}

impl ObjectFinalize for CustomFinalize {
  fn finalize(self, mut env: Env) -> Result<()> {
    let external_size = i64::try_from(self.inner.len())
      .map_err(|_| Error::new(Status::InvalidArg, "image buffer is too large"))?;
    env.adjust_external_memory(-external_size)?;
    Ok(())
  }
}

First, you can set custom_finalize attribute in #[napi] macro, and NAPI-RS will not generate the default ObjectFinalize for the Rust struct.

Then, you can implement ObjectFinalize yourself for the Rust struct.

In this case, the CustomFinalize struct increases external memory in the constructor and decreases it in fn finalize.

instance of

There is fn instance_of on all #[napi] class:

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi(constructor)]
pub struct NativeClass {}

#[napi]
pub fn is_native_class_instance(env: &Env, value: Unknown) -> Result<bool> {
  NativeClass::instance_of(env, &value) 
}
main.mjs
js
import { NativeClass, isNativeClassInstance } from './index.js'

const nc = new NativeClass()
console.log(isNativeClassInstance(nc)) // true
console.log(isNativeClassInstance(1)) // false