Repository: streamich/memfs
Pull request #914 (2023-06-16 01:36:29)

Node fs fixes and improvements
Would like to merge these Node/Volume changes into the `next` branch before the FSA code.

- Minor fixes in `Volume` (writing at seek offset)
- Improvements in `Volume` types (accept typed arrays, add missing option parameters)
- Refactors `.promises` code into `/node` folder
- Adds `fs` types into `/node/types`
- Exports new `memfs` utility from the top level


Patch:
diff --git a/src/index.ts b/src/index.ts
index 7a7ec655..2ce08fc0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,9 +9,9 @@ import {
   IWriteStream,
   DirectoryJSON,
 } from './volume';
-import { IPromisesAPI } from './promises';
 const { fsSyncMethods, fsAsyncMethods } = require('fs-monkey/lib/util/lists');
 import { constants } from './constants';
+import type { FsPromisesApi } from './node/types';
 const { F_OK, R_OK, W_OK, X_OK } = constants;
 
 export { DirectoryJSON };
@@ -28,7 +28,7 @@ export interface IFs extends _Volume {
   FSWatcher: new () => FSWatcher;
   ReadStream: new (...args) => IReadStream;
   WriteStream: new (...args) => IWriteStream;
-  promises: IPromisesAPI;
+  promises: FsPromisesApi;
   _toUnixTimestamp;
 }
 
@@ -46,12 +46,31 @@ export function createFsFromVolume(vol: _Volume): IFs {
   fs.promises = vol.promises;
 
   fs._toUnixTimestamp = toUnixTimestamp;
+  (fs as any).__vol = vol;
 
   return fs;
 }
 
 export const fs: IFs = createFsFromVolume(vol);
+
+/**
+ * Creates a new file system instance.
+ *
+ * @param json File system structure expressed as a JSON object.
+ *        Use `null` for empty directories and empty string for empty files.
+ * @param cwd Current working directory. The JSON structure will be created
+ *        relative to this path.
+ * @returns A `memfs` file system instance, which is a drop-in replacement for
+ *          the `fs` module.
+ */
+export const memfs = (json: DirectoryJSON = {}, cwd: string = '/'): IFs => {
+  const volume = Volume.fromJSON(json, cwd);
+  const fs = createFsFromVolume(volume);
+  return fs;
+};
+
+export type IFsWithVolume = IFs & { __vol: _Volume };
+
 declare let module;
 module.exports = { ...module.exports, ...fs };
-
 module.exports.semantic = true;
diff --git a/src/node.ts b/src/node.ts
index 9ac8e756..e0587531 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -185,10 +185,14 @@ export class Node extends EventEmitter {
   }
 
   // Returns the number of bytes read.
-  read(buf: Buffer | Uint8Array, off: number = 0, len: number = buf.byteLength, pos: number = 0): number {
+  read(
+    buf: Buffer | ArrayBufferView | DataView,
+    off: number = 0,
+    len: number = buf.byteLength,
+    pos: number = 0,
+  ): number {
     this.atime = new Date();
     if (!this.buf) this.buf = bufferAllocUnsafe(0);
-
     let actualLen = len;
     if (actualLen > buf.byteLength) {
       actualLen = buf.byteLength;
@@ -196,8 +200,8 @@ export class Node extends EventEmitter {
     if (actualLen + pos > this.buf.length) {
       actualLen = this.buf.length - pos;
     }
-
-    this.buf.copy(buf as Buffer, off, pos, pos + actualLen);
+    const buf2 = buf instanceof Buffer ? buf : Buffer.from(buf.buffer);
+    this.buf.copy(buf2, off, pos, pos + actualLen);
     return actualLen;
   }
 
@@ -473,7 +477,7 @@ export class File {
    * A cursor/offset position in a file, where data will be written on write.
    * User can "seek" this position.
    */
-  position: number = 0;
+  position: number;
 
   // Flags used when opening the file.
   flags: number;
@@ -491,6 +495,8 @@ export class File {
     this.node = node;
     this.flags = flags;
     this.fd = fd;
+    this.position = 0;
+    if (this.flags & O_APPEND) this.position = this.getSize();
   }
 
   getString(encoding = 'utf8'): string {
@@ -527,13 +533,17 @@ export class File {
 
   write(buf: Buffer, offset: number = 0, length: number = buf.length, position?: number): number {
     if (typeof position !== 'number') position = this.position;
-    if (this.flags & O_APPEND) position = this.getSize();
     const bytes = this.node.write(buf, offset, length, position);
     this.position = position + bytes;
     return bytes;
   }
 
-  read(buf: Buffer | Uint8Array, offset: number = 0, length: number = buf.byteLength, position?: number): number {
+  read(
+    buf: Buffer | ArrayBufferView | DataView,
+    offset: number = 0,
+    length: number = buf.byteLength,
+    position?: number,
+  ): number {
     if (typeof position !== 'number') position = this.position;
     const bytes = this.node.read(buf, offset, length, position);
     this.position = position + bytes;
diff --git a/src/node/FileHandle.ts b/src/node/FileHandle.ts
new file mode 100644
index 00000000..81467d6b
--- /dev/null
+++ b/src/node/FileHandle.ts
@@ -0,0 +1,88 @@
+import { promisify } from './util';
+import type * as opts from './types/options';
+import type { IFileHandle, IStats, TData, TDataOut, TMode, TTime } from './types/misc';
+import type { FsCallbackApi } from './types';
+
+export class FileHandle implements IFileHandle {
+  private fs: FsCallbackApi;
+
+  fd: number;
+
+  constructor(fs: FsCallbackApi, fd: number) {
+    this.fs = fs;
+    this.fd = fd;
+  }
+
+  appendFile(data: TData, options?: opts.IAppendFileOptions | string): Promise<void> {
+    return promisify(this.fs, 'appendFile')(this.fd, data, options);
+  }
+
+  chmod(mode: TMode): Promise<void> {
+    return promisify(this.fs, 'fchmod')(this.fd, mode);
+  }
+
+  chown(uid: number, gid: number): Promise<void> {
+    return promisify(this.fs, 'fchown')(this.fd, uid, gid);
+  }
+
+  close(): Promise<void> {
+    return promisify(this.fs, 'close')(this.fd);
+  }
+
+  datasync(): Promise<void> {
+    return promisify(this.fs, 'fdatasync')(this.fd);
+  }
+
+  read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise<TFileHandleReadResult> {
+    return promisify(this.fs, 'read', bytesRead => ({ bytesRead, buffer }))(this.fd, buffer, offset, length, position);
+  }
+
+  readFile(options?: opts.IReadFileOptions | string): Promise<TDataOut> {
+    return promisify(this.fs, 'readFile')(this.fd, options);
+  }
+
+  stat(options?: opts.IFStatOptions): Promise<IStats> {
+    return promisify(this.fs, 'fstat')(this.fd, options);
+  }
+
+  sync(): Promise<void> {
+    return promisify(this.fs, 'fsync')(this.fd);
+  }
+
+  truncate(len?: number): Promise<void> {
+    return promisify(this.fs, 'ftruncate')(this.fd, len);
+  }
+
+  utimes(atime: TTime, mtime: TTime): Promise<void> {
+    return promisify(this.fs, 'futimes')(this.fd, atime, mtime);
+  }
+
+  write(
+    buffer: Buffer | Uint8Array,
+    offset?: number,
+    length?: number,
+    position?: number,
+  ): Promise<TFileHandleWriteResult> {
+    return promisify(this.fs, 'write', bytesWritten => ({ bytesWritten, buffer }))(
+      this.fd,
+      buffer,
+      offset,
+      length,
+      position,
+    );
+  }
+
+  writeFile(data: TData, options?: opts.IWriteFileOptions): Promise<void> {
+    return promisify(this.fs, 'writeFile')(this.fd, data, options);
+  }
+}
+
+export interface TFileHandleReadResult {
+  bytesRead: number;
+  buffer: Buffer | Uint8Array;
+}
+
+export interface TFileHandleWriteResult {
+  bytesWritten: number;
+  buffer: Buffer | Uint8Array;
+}
diff --git a/src/node/promises.ts b/src/node/promises.ts
new file mode 100644
index 00000000..8c1ee2bc
--- /dev/null
+++ b/src/node/promises.ts
@@ -0,0 +1,116 @@
+import { FileHandle } from './FileHandle';
+import { promisify } from './util';
+import type * as opts from './types/options';
+import type * as misc from './types/misc';
+import type { FsCallbackApi, FsPromisesApi } from './types';
+
+export function createPromisesApi(vol: FsCallbackApi): null | FsPromisesApi {
+  if (typeof Promise === 'undefined') return null;
+  return {
+    FileHandle,
+
+    access(path: misc.PathLike, mode?: number): Promise<void> {
+      return promisify(vol, 'access')(path, mode);
+    },
+
+    appendFile(path: misc.TFileHandle, data: misc.TData, options?: opts.IAppendFileOptions | string): Promise<void> {
+      return promisify(vol, 'appendFile')(
+        path instanceof FileHandle ? path.fd : (path as misc.PathLike),
+        data,
+        options,
+      );
+    },
+
+    chmod(path: misc.PathLike, mode: misc.TMode): Promise<void> {
+      return promisify(vol, 'chmod')(path, mode);
+    },
+
+    chown(path: misc.PathLike, uid: number, gid: number): Promise<void> {
+      return promisify(vol, 'chown')(path, uid, gid);
+    },
+
+    copyFile(src: misc.PathLike, dest: misc.PathLike, flags?: misc.TFlagsCopy): Promise<void> {
+      return promisify(vol, 'copyFile')(src, dest, flags);
+    },
+
+    lchmod(path: misc.PathLike, mode: misc.TMode): Promise<void> {
+      return promisify(vol, 'lchmod')(path, mode);
+    },
+
+    lchown(path: misc.PathLike, uid: number, gid: number): Promise<void> {
+      return promisify(vol, 'lchown')(path, uid, gid);
+    },
+
+    link(existingPath: misc.PathLike, newPath: misc.PathLike): Promise<void> {
+      return promisify(vol, 'link')(existingPath, newPath);
+    },
+
+    lstat(path: misc.PathLike, options?: opts.IStatOptions): Promise<misc.IStats> {
+      return promisify(vol, 'lstat')(path, options);
+    },
+
+    mkdir(path: misc.PathLike, options?: misc.TMode | opts.IMkdirOptions): Promise<void> {
+      return promisify(vol, 'mkdir')(path, options);
+    },
+
+    mkdtemp(prefix: string, options?: opts.IOptions) {
+      return promisify(vol, 'mkdtemp')(prefix, options);
+    },
+
+    open(path: misc.PathLike, flags: misc.TFlags, mode?: misc.TMode) {
+      return promisify(vol, 'open', fd => new FileHandle(vol, fd))(path, flags, mode);
+    },
+
+    readdir(path: misc.PathLike, options?: opts.IReaddirOptions | string): Promise<misc.TDataOut[] | misc.IDirent[]> {
+      return promisify(vol, 'readdir')(path, options);
+    },
+
+    readFile(id: misc.TFileHandle, options?: opts.IReadFileOptions | string): Promise<misc.TDataOut> {
+      return promisify(vol, 'readFile')(id instanceof FileHandle ? id.fd : (id as misc.PathLike), options);
+    },
+
+    readlink(path: misc.PathLike, options?: opts.IOptions): Promise<misc.TDataOut> {
+      return promisify(vol, 'readlink')(path, options);
+    },
+
+    realpath(path: misc.PathLike, options?: opts.IRealpathOptions | string): Promise<misc.TDataOut> {
+      return promisify(vol, 'realpath')(path, options);
+    },
+
+    rename(oldPath: misc.PathLike, newPath: misc.PathLike): Promise<void> {
+      return promisify(vol, 'rename')(oldPath, newPath);
+    },
+
+    rmdir(path: misc.PathLike, options?: opts.IRmdirOptions): Promise<void> {
+      return promisify(vol, 'rmdir')(path, options);
+    },
+
+    rm(path: misc.PathLike, options?: opts.IRmOptions): Promise<void> {
+      return promisify(vol, 'rm')(path, options);
+    },
+
+    stat(path: misc.PathLike, options?: opts.IStatOptions): Promise<misc.IStats> {
+      return promisify(vol, 'stat')(path, options);
+    },
+
+    symlink(target: misc.PathLike, path: misc.PathLike, type?: misc.symlink.Type): Promise<void> {
+      return promisify(vol, 'symlink')(target, path, type);
+    },
+
+    truncate(path: misc.PathLike, len?: number): Promise<void> {
+      return promisify(vol, 'truncate')(path, len);
+    },
+
+    unlink(path: misc.PathLike): Promise<void> {
+      return promisify(vol, 'unlink')(path);
+    },
+
+    utimes(path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise<void> {
+      return promisify(vol, 'utimes')(path, atime, mtime);
+    },
+
+    writeFile(id: misc.TFileHandle, data: misc.TData, options?: opts.IWriteFileOptions): Promise<void> {
+      return promisify(vol, 'writeFile')(id instanceof FileHandle ? id.fd : (id as misc.PathLike), data, options);
+    },
+  };
+}
diff --git a/src/node/types/callback.ts b/src/node/types/callback.ts
new file mode 100644
index 00000000..ccde4b7d
--- /dev/null
+++ b/src/node/types/callback.ts
@@ -0,0 +1,124 @@
+import type { PathLike, symlink } from 'fs';
+import type * as misc from './misc';
+import type * as opts from './options';
+
+export interface FsCallbackApi {
+  open(path: PathLike, flags: misc.TFlags, /* ... */ callback: misc.TCallback<number>);
+  open(path: PathLike, flags: misc.TFlags, mode: misc.TMode, callback: misc.TCallback<number>);
+  close(fd: number, callback: misc.TCallback<void>): void;
+  read(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset: number,
+    length: number,
+    position: number,
+    callback: (err?: Error | null, bytesRead?: number, buffer?: Buffer | ArrayBufferView | DataView) => void,
+  ): void;
+  readFile(id: misc.TFileId, callback: misc.TCallback<misc.TDataOut>);
+  readFile(id: misc.TFileId, options: opts.IReadFileOptions | string, callback: misc.TCallback<misc.TDataOut>);
+  write(fd: number, buffer: Buffer | ArrayBufferView | DataView, callback: (...args) => void);
+  write(fd: number, buffer: Buffer | ArrayBufferView | DataView, offset: number, callback: (...args) => void);
+  write(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset: number,
+    length: number,
+    callback: (...args) => void,
+  );
+  write(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset: number,
+    length: number,
+    position: number,
+    callback: (...args) => void,
+  );
+  write(fd: number, str: string, callback: (...args) => void);
+  write(fd: number, str: string, position: number, callback: (...args) => void);
+  write(fd: number, str: string, position: number, encoding: BufferEncoding, callback: (...args) => void);
+  writeFile(id: misc.TFileId, data: misc.TData, callback: misc.TCallback<void>);
+  writeFile(
+    id: misc.TFileId,
+    data: misc.TData,
+    options: opts.IWriteFileOptions | string,
+    callback: misc.TCallback<void>,
+  );
+  copyFile(src: PathLike, dest: PathLike, callback: misc.TCallback<void>);
+  copyFile(src: PathLike, dest: PathLike, flags: misc.TFlagsCopy, callback: misc.TCallback<void>);
+  link(existingPath: PathLike, newPath: PathLike, callback: misc.TCallback<void>): void;
+  unlink(path: PathLike, callback: misc.TCallback<void>): void;
+  symlink(target: PathLike, path: PathLike, callback: misc.TCallback<void>);
+  symlink(target: PathLike, path: PathLike, type: symlink.Type, callback: misc.TCallback<void>);
+  realpath(path: PathLike, callback: misc.TCallback<misc.TDataOut>);
+  realpath(path: PathLike, options: opts.IRealpathOptions | string, callback: misc.TCallback<misc.TDataOut>);
+  lstat(path: PathLike, callback: misc.TCallback<misc.IStats>): void;
+  lstat(path: PathLike, options: opts.IStatOptions, callback: misc.TCallback<misc.IStats>): void;
+  stat(path: PathLike, callback: misc.TCallback<misc.IStats>): void;
+  stat(path: PathLike, options: opts.IStatOptions, callback: misc.TCallback<misc.IStats>): void;
+  fstat(fd: number, callback: misc.TCallback<misc.IStats>): void;
+  fstat(fd: number, options: opts.IFStatOptions, callback: misc.TCallback<misc.IStats>): void;
+  rename(oldPath: PathLike, newPath: PathLike, callback: misc.TCallback<void>): void;
+  exists(path: PathLike, callback: (exists: boolean) => void): void;
+  access(path: PathLike, callback: misc.TCallback<void>);
+  access(path: PathLike, mode: number, callback: misc.TCallback<void>);
+  appendFile(id: misc.TFileId, data: misc.TData, callback: misc.TCallback<void>);
+  appendFile(
+    id: misc.TFileId,
+    data: misc.TData,
+    options: opts.IAppendFileOptions | string,
+    callback: misc.TCallback<void>,
+  );
+  readdir(path: PathLike, callback: misc.TCallback<misc.TDataOut[] | misc.IDirent[]>);
+  readdir(
+    path: PathLike,
+    options: opts.IReaddirOptions | string,
+    callback: misc.TCallback<misc.TDataOut[] | misc.IDirent[]>,
+  );
+  readlink(path: PathLike, callback: misc.TCallback<misc.TDataOut>);
+  readlink(path: PathLike, options: opts.IOptions, callback: misc.TCallback<misc.TDataOut>);
+  fsyncSync(fd: number): void;
+  fsync(fd: number, callback: misc.TCallback<void>): void;
+  fdatasync(fd: number, callback: misc.TCallback<void>): void;
+  ftruncate(fd: number, callback: misc.TCallback<void>);
+  ftruncate(fd: number, len: number, callback: misc.TCallback<void>);
+  truncate(id: misc.TFileId, callback: misc.TCallback<void>);
+  truncate(id: misc.TFileId, len: number, callback: misc.TCallback<void>);
+  futimes(fd: number, atime: misc.TTime, mtime: misc.TTime, callback: misc.TCallback<void>): void;
+  utimes(path: PathLike, atime: misc.TTime, mtime: misc.TTime, callback: misc.TCallback<void>): void;
+  mkdir(path: PathLike, callback: misc.TCallback<void>);
+  mkdir(
+    path: PathLike,
+    mode: misc.TMode | (opts.IMkdirOptions & { recursive?: false }),
+    callback: misc.TCallback<void>,
+  );
+  mkdir(path: PathLike, mode: opts.IMkdirOptions & { recursive: true }, callback: misc.TCallback<string>);
+  mkdir(path: PathLike, mode: misc.TMode | opts.IMkdirOptions, callback: misc.TCallback<string>);
+  mkdirp(path: PathLike, callback: misc.TCallback<string>);
+  mkdirp(path: PathLike, mode: misc.TMode, callback: misc.TCallback<string>);
+  mkdtemp(prefix: string, callback: misc.TCallback<void>): void;
+  mkdtemp(prefix: string, options: opts.IOptions, callback: misc.TCallback<void>);
+  rmdir(path: PathLike, callback: misc.TCallback<void>);
+  rmdir(path: PathLike, options: opts.IRmdirOptions, callback: misc.TCallback<void>);
+  rm(path: PathLike, callback: misc.TCallback<void>): void;
+  rm(path: PathLike, options: opts.IRmOptions, callback: misc.TCallback<void>): void;
+  fchmod(fd: number, mode: misc.TMode, callback: misc.TCallback<void>): void;
+  chmod(path: PathLike, mode: misc.TMode, callback: misc.TCallback<void>): void;
+  lchmod(path: PathLike, mode: misc.TMode, callback: misc.TCallback<void>): void;
+  fchown(fd: number, uid: number, gid: number, callback: misc.TCallback<void>): void;
+  chown(path: PathLike, uid: number, gid: number, callback: misc.TCallback<void>): void;
+  lchown(path: PathLike, uid: number, gid: number, callback: misc.TCallback<void>): void;
+  watchFile(path: PathLike, listener: (curr: misc.IStats, prev: misc.IStats) => void): misc.IStatWatcher;
+  watchFile(
+    path: PathLike,
+    options: opts.IWatchFileOptions,
+    listener: (curr: misc.IStats, prev: misc.IStats) => void,
+  ): misc.IStatWatcher;
+  unwatchFile(path: PathLike, listener?: (curr: misc.IStats, prev: misc.IStats) => void): void;
+  createReadStream(path: PathLike, options?: opts.IReadStreamOptions | string): misc.IReadStream;
+  createWriteStream(path: PathLike, options?: opts.IWriteStreamOptions | string): misc.IWriteStream;
+  watch(
+    path: PathLike,
+    options?: opts.IWatchOptions | string,
+    listener?: (eventType: string, filename: string) => void,
+  ): misc.IFSWatcher;
+}
diff --git a/src/node/types/index.ts b/src/node/types/index.ts
new file mode 100644
index 00000000..f47f5ba4
--- /dev/null
+++ b/src/node/types/index.ts
@@ -0,0 +1,9 @@
+import type { FsSynchronousApi } from './sync';
+import type { FsCallbackApi } from './callback';
+import type { FsPromisesApi } from './promises';
+
+export { FsSynchronousApi, FsCallbackApi, FsPromisesApi };
+
+export interface FsApi extends FsCallbackApi, FsSynchronousApi {
+  promises: FsPromisesApi;
+}
diff --git a/src/node/types/misc.ts b/src/node/types/misc.ts
new file mode 100644
index 00000000..facf4c9a
--- /dev/null
+++ b/src/node/types/misc.ts
@@ -0,0 +1,138 @@
+import type { PathLike, symlink } from 'fs';
+import type { constants } from '../../constants';
+import type { EventEmitter } from 'events';
+import type { TSetTimeout } from '../../setTimeoutUnref';
+import type {
+  IAppendFileOptions,
+  IReadFileOptions,
+  IReadStreamOptions,
+  IStatOptions,
+  IWriteFileOptions,
+  IWriteStreamOptions,
+} from './options';
+import type { Readable, Writable } from 'stream';
+
+export { PathLike, symlink };
+
+export type TDataOut = string | Buffer; // Data formats we give back to users.
+export type TEncodingExtended = BufferEncoding | 'buffer';
+export type TFileId = PathLike | number; // Number is used as a file descriptor.
+export type TData = TDataOut | ArrayBufferView | DataView; // Data formats users can give us.
+export type TFlags = string | number;
+export type TMode = string | number; // Mode can be a String, although docs say it should be a Number.
+export type TTime = number | string | Date;
+export type TCallback<TData> = (error?: IError | null, data?: TData) => void;
+
+export interface IError extends Error {
+  code?: string;
+}
+
+export type TFlagsCopy =
+  | typeof constants.COPYFILE_EXCL
+  | typeof constants.COPYFILE_FICLONE
+  | typeof constants.COPYFILE_FICLONE_FORCE;
+
+export type TStatNumber = number | bigint;
+
+export interface IStats<T = TStatNumber> {
+  uid: T;
+  gid: T;
+  rdev: T;
+  blksize: T;
+  ino: T;
+  size: T;
+  blocks: T;
+  atime: Date;
+  mtime: Date;
+  ctime: Date;
+  birthtime: Date;
+  atimeMs: T;
+  mtimeMs: T;
+  ctimeMs: T;
+  birthtimeMs: T;
+  dev: T;
+  mode: T;
+  nlink: T;
+  isDirectory(): boolean;
+  isFile(): boolean;
+  isBlockDevice(): boolean;
+  isCharacterDevice(): boolean;
+  isSymbolicLink(): boolean;
+  isFIFO(): boolean;
+  isSocket(): boolean;
+}
+
+export interface IDirent {
+  name: TDataOut;
+  isDirectory(): boolean;
+  isFile(): boolean;
+  isBlockDevice(): boolean;
+  isCharacterDevice(): boolean;
+  isSymbolicLink(): boolean;
+  isFIFO(): boolean;
+  isSocket(): boolean;
+}
+
+export interface IStatWatcher extends EventEmitter {
+  filename: string;
+  interval: number;
+  timeoutRef?;
+  setTimeout: TSetTimeout;
+  prev: IStats;
+  start(path: string, persistent?: boolean, interval?: number): void;
+  stop(): void;
+}
+
+export interface IReadStream extends Readable {
+  new (path: PathLike, options: IReadStreamOptions);
+  open();
+  close(callback: TCallback<void>);
+  bytesRead: number;
+  path: string;
+}
+
+export interface IWriteStream extends Writable {
+  bytesWritten: number;
+  path: string;
+  new (path: PathLike, options: IWriteStreamOptions);
+  open();
+  close();
+}
+
+export interface IFSWatcher extends EventEmitter {
+  start(path: PathLike, persistent?: boolean, recursive?: boolean, encoding?: BufferEncoding): void;
+  close(): void;
+}
+
+export interface IFileHandle {
+  fd: number;
+  appendFile(data: TData, options?: IAppendFileOptions | string): Promise<void>;
+  chmod(mode: TMode): Promise<void>;
+  chown(uid: number, gid: number): Promise<void>;
+  close(): Promise<void>;
+  datasync(): Promise<void>;
+  read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise<TFileHandleReadResult>;
+  readFile(options?: IReadFileOptions | string): Promise<TDataOut>;
+  stat(options?: IStatOptions): Promise<IStats>;
+  truncate(len?: number): Promise<void>;
+  utimes(atime: TTime, mtime: TTime): Promise<void>;
+  write(
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset?: number,
+    length?: number,
+    position?: number,
+  ): Promise<TFileHandleWriteResult>;
+  writeFile(data: TData, options?: IWriteFileOptions): Promise<void>;
+}
+
+export type TFileHandle = PathLike | IFileHandle;
+
+export interface TFileHandleReadResult {
+  bytesRead: number;
+  buffer: Buffer | Uint8Array;
+}
+
+export interface TFileHandleWriteResult {
+  bytesWritten: number;
+  buffer: Buffer | Uint8Array;
+}
diff --git a/src/node/types/options.ts b/src/node/types/options.ts
new file mode 100644
index 00000000..b94e556f
--- /dev/null
+++ b/src/node/types/options.ts
@@ -0,0 +1,85 @@
+import type { TEncodingExtended, TFlags, TMode } from './misc';
+
+export interface IOptions {
+  encoding?: BufferEncoding | TEncodingExtended;
+}
+
+export interface IFileOptions extends IOptions {
+  mode?: TMode;
+  flag?: TFlags;
+}
+
+export interface IWriteFileOptions extends IFileOptions {}
+
+export interface IReadFileOptions extends IOptions {
+  flag?: string;
+}
+
+export interface IRealpathOptions {
+  encoding?: TEncodingExtended;
+}
+
+export interface IAppendFileOptions extends IFileOptions {}
+
+export interface IStatOptions {
+  bigint?: boolean;
+  throwIfNoEntry?: boolean;
+}
+
+export interface IFStatOptions {
+  bigint?: boolean;
+}
+
+export interface IAppendFileOptions extends IFileOptions {}
+
+export interface IReaddirOptions extends IOptions {
+  withFileTypes?: boolean;
+}
+
+export interface IMkdirOptions {
+  mode?: TMode;
+  recursive?: boolean;
+}
+
+export interface IRmdirOptions {
+  /** @deprecated */
+  recursive?: boolean;
+  maxRetries?: number;
+  retryDelay?: number;
+}
+
+export interface IRmOptions {
+  force?: boolean;
+  maxRetries?: number;
+  recursive?: boolean;
+  retryDelay?: number;
+}
+
+export interface IWatchFileOptions {
+  persistent?: boolean;
+  interval?: number;
+}
+
+export interface IReadStreamOptions {
+  flags?: TFlags;
+  encoding?: BufferEncoding;
+  fd?: number;
+  mode?: TMode;
+  autoClose?: boolean;
+  start?: number;
+  end?: number;
+}
+
+export interface IWriteStreamOptions {
+  flags?: TFlags;
+  defaultEncoding?: BufferEncoding;
+  fd?: number;
+  mode?: TMode;
+  autoClose?: boolean;
+  start?: number;
+}
+
+export interface IWatchOptions extends IOptions {
+  persistent?: boolean;
+  recursive?: boolean;
+}
diff --git a/src/node/types/promises.ts b/src/node/types/promises.ts
new file mode 100644
index 00000000..35269f80
--- /dev/null
+++ b/src/node/types/promises.ts
@@ -0,0 +1,31 @@
+import * as misc from './misc';
+import * as opts from './options';
+
+export interface FsPromisesApi {
+  FileHandle: new (...args: unknown[]) => misc.IFileHandle;
+  access(path: misc.PathLike, mode?: number): Promise<void>;
+  appendFile(path: misc.TFileHandle, data: misc.TData, options?: opts.IAppendFileOptions | string): Promise<void>;
+  chmod(path: misc.PathLike, mode: misc.TMode): Promise<void>;
+  chown(path: misc.PathLike, uid: number, gid: number): Promise<void>;
+  copyFile(src: misc.PathLike, dest: misc.PathLike, flags?: misc.TFlagsCopy): Promise<void>;
+  lchmod(path: misc.PathLike, mode: misc.TMode): Promise<void>;
+  lchown(path: misc.PathLike, uid: number, gid: number): Promise<void>;
+  link(existingPath: misc.PathLike, newPath: misc.PathLike): Promise<void>;
+  lstat(path: misc.PathLike, options?: opts.IStatOptions): Promise<misc.IStats>;
+  mkdir(path: misc.PathLike, options?: misc.TMode | opts.IMkdirOptions): Promise<void>;
+  mkdtemp(prefix: string, options?: opts.IOptions): Promise<misc.TDataOut>;
+  open(path: misc.PathLike, flags: misc.TFlags, mode?: misc.TMode): Promise<misc.IFileHandle>;
+  readdir(path: misc.PathLike, options?: opts.IReaddirOptions | string): Promise<misc.TDataOut[] | misc.IDirent[]>;
+  readFile(id: misc.TFileHandle, options?: opts.IReadFileOptions | string): Promise<misc.TDataOut>;
+  readlink(path: misc.PathLike, options?: opts.IOptions): Promise<misc.TDataOut>;
+  realpath(path: misc.PathLike, options?: opts.IRealpathOptions | string): Promise<misc.TDataOut>;
+  rename(oldPath: misc.PathLike, newPath: misc.PathLike): Promise<void>;
+  rmdir(path: misc.PathLike, options?: opts.IRmdirOptions): Promise<void>;
+  rm(path: misc.PathLike, options?: opts.IRmOptions): Promise<void>;
+  stat(path: misc.PathLike, options?: opts.IStatOptions): Promise<misc.IStats>;
+  symlink(target: misc.PathLike, path: misc.PathLike, type?: misc.symlink.Type): Promise<void>;
+  truncate(path: misc.PathLike, len?: number): Promise<void>;
+  unlink(path: misc.PathLike): Promise<void>;
+  utimes(path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise<void>;
+  writeFile(id: misc.TFileHandle, data: misc.TData, options?: opts.IWriteFileOptions): Promise<void>;
+}
diff --git a/src/node/types/sync.ts b/src/node/types/sync.ts
new file mode 100644
index 00000000..0e7a1eb3
--- /dev/null
+++ b/src/node/types/sync.ts
@@ -0,0 +1,71 @@
+import type { PathLike, symlink } from 'fs';
+import type * as misc from './misc';
+import type * as opts from './options';
+
+export interface FsSynchronousApi {
+  openSync(path: PathLike, flags: misc.TFlags, mode?: misc.TMode): number;
+  closeSync(fd: number): void;
+  readSync(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset: number,
+    length: number,
+    position: number,
+  ): number;
+  readFileSync(file: misc.TFileId, options?: opts.IReadFileOptions | string): misc.TDataOut;
+  writeSync(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset?: number,
+    length?: number,
+    position?: number,
+  ): number;
+  writeSync(fd: number, str: string, position?: number, encoding?: BufferEncoding): number;
+  writeFileSync(id: misc.TFileId, data: misc.TData, options?: opts.IWriteFileOptions): void;
+  copyFileSync(src: PathLike, dest: PathLike, flags?: misc.TFlagsCopy): void;
+  linkSync(existingPath: PathLike, newPath: PathLike): void;
+  unlinkSync(path: PathLike): void;
+  symlinkSync(target: PathLike, path: PathLike, type?: symlink.Type): void;
+  realpathSync(path: PathLike, options?: opts.IRealpathOptions | string): misc.TDataOut;
+  lstatSync(path: PathLike): misc.IStats<number>;
+  lstatSync(path: PathLike, options: { throwIfNoEntry?: true | undefined }): misc.IStats<number>;
+  lstatSync(path: PathLike, options: { bigint: false; throwIfNoEntry?: true | undefined }): misc.IStats<number>;
+  lstatSync(path: PathLike, options: { bigint: true; throwIfNoEntry?: true | undefined }): misc.IStats<bigint>;
+  lstatSync(path: PathLike, options: { throwIfNoEntry: false }): misc.IStats<number> | undefined;
+  lstatSync(path: PathLike, options: { bigint: false; throwIfNoEntry: false }): misc.IStats<number> | undefined;
+  lstatSync(path: PathLike, options: { bigint: true; throwIfNoEntry: false }): misc.IStats<bigint> | undefined;
+  statSync(path: PathLike): misc.IStats<number>;
+  statSync(path: PathLike, options: { throwIfNoEntry?: true }): misc.IStats<number>;
+  statSync(path: PathLike, options: { throwIfNoEntry: false }): misc.IStats<number> | undefined;
+  statSync(path: PathLike, options: { bigint: false; throwIfNoEntry?: true }): misc.IStats<number>;
+  statSync(path: PathLike, options: { bigint: true; throwIfNoEntry?: true }): misc.IStats<bigint>;
+  statSync(path: PathLike, options: { bigint: false; throwIfNoEntry: false }): misc.IStats<number> | undefined;
+  statSync(path: PathLike, options: { bigint: true; throwIfNoEntry: false }): misc.IStats<bigint> | undefined;
+  fstatSync(fd: number): misc.IStats<number>;
+  fstatSync(fd: number, options: { bigint: false }): misc.IStats<number>;
+  fstatSync(fd: number, options: { bigint: true }): misc.IStats<bigint>;
+  renameSync(oldPath: PathLike, newPath: PathLike): void;
+  existsSync(path: PathLike): boolean;
+  accessSync(path: PathLike, mode?: number): void;
+  appendFileSync(id: misc.TFileId, data: misc.TData, options?: opts.IAppendFileOptions | string): void;
+  readdirSync(path: PathLike, options?: opts.IReaddirOptions | string): misc.TDataOut[] | misc.IDirent[];
+  readlinkSync(path: PathLike, options?: opts.IOptions): misc.TDataOut;
+  fdatasyncSync(fd: number): void;
+  ftruncateSync(fd: number, len?: number): void;
+  truncateSync(id: misc.TFileId, len?: number): void;
+  futimesSync(fd: number, atime: misc.TTime, mtime: misc.TTime): void;
+  utimesSync(path: PathLike, atime: misc.TTime, mtime: misc.TTime): void;
+  mkdirSync(path: PathLike, options: opts.IMkdirOptions & { recursive: true }): string | undefined;
+  mkdirSync(path: PathLike, options?: misc.TMode | (opts.IMkdirOptions & { recursive?: false })): void;
+  mkdirSync(path: PathLike, options?: misc.TMode | opts.IMkdirOptions): string | undefined;
+  mkdirpSync(path: PathLike, mode?: misc.TMode): void;
+  mkdtempSync(prefix: string, options?: opts.IOptions): misc.TDataOut;
+  rmdirSync(path: PathLike, options?: opts.IRmdirOptions): void;
+  rmSync(path: PathLike, options?: opts.IRmOptions): void;
+  fchmodSync(fd: number, mode: misc.TMode): void;
+  chmodSync(path: PathLike, mode: misc.TMode): void;
+  lchmodSync(path: PathLike, mode: misc.TMode): void;
+  fchownSync(fd: number, uid: number, gid: number): void;
+  chownSync(path: PathLike, uid: number, gid: number): void;
+  lchownSync(path: PathLike, uid: number, gid: number): void;
+}
diff --git a/src/node/util.ts b/src/node/util.ts
new file mode 100644
index 00000000..aff4473a
--- /dev/null
+++ b/src/node/util.ts
@@ -0,0 +1,15 @@
+import type { FsCallbackApi } from './types';
+
+export function promisify(
+  fs: FsCallbackApi,
+  fn: string,
+  getResult: (result: any) => any = input => input,
+): (...args) => Promise<any> {
+  return (...args) =>
+    new Promise((resolve, reject) => {
+      fs[fn].bind(fs)(...args, (error, result) => {
+        if (error) return reject(error);
+        return resolve(getResult(result));
+      });
+    });
+}
diff --git a/src/promises.ts b/src/promises.ts
deleted file mode 100644
index 5bd2fca9..00000000
--- a/src/promises.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-import {
-  Volume,
-  TData,
-  TMode,
-  TFlags,
-  TFlagsCopy,
-  TTime,
-  IOptions,
-  IAppendFileOptions,
-  IMkdirOptions,
-  IReaddirOptions,
-  IReadFileOptions,
-  IRealpathOptions,
-  IWriteFileOptions,
-  IStatOptions,
-  IRmOptions,
-  IFStatOptions,
-} from './volume';
-import Stats from './Stats';
-import Dirent from './Dirent';
-import { TDataOut } from './encoding';
-import { PathLike, symlink } from 'fs';
-
-function promisify(
-  vol: Volume,
-  fn: string,
-  getResult: (result: any) => any = input => input,
-): (...args) => Promise<any> {
-  return (...args) =>
-    new Promise((resolve, reject) => {
-      vol[fn].bind(vol)(...args, (error, result) => {
-        if (error) return reject(error);
-        return resolve(getResult(result));
-      });
-    });
-}
-
-export interface TFileHandleReadResult {
-  bytesRead: number;
-  buffer: Buffer | Uint8Array;
-}
-
-export interface TFileHandleWriteResult {
-  bytesWritten: number;
-  buffer: Buffer | Uint8Array;
-}
-
-export interface IFileHandle {
-  fd: number;
-  appendFile(data: TData, options?: IAppendFileOptions | string): Promise<void>;
-  chmod(mode: TMode): Promise<void>;
-  chown(uid: number, gid: number): Promise<void>;
-  close(): Promise<void>;
-  datasync(): Promise<void>;
-  read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise<TFileHandleReadResult>;
-  readFile(options?: IReadFileOptions | string): Promise<TDataOut>;
-  stat(options?: IStatOptions): Promise<Stats>;
-  truncate(len?: number): Promise<void>;
-  utimes(atime: TTime, mtime: TTime): Promise<void>;
-  write(
-    buffer: Buffer | Uint8Array,
-    offset?: number,
-    length?: number,
-    position?: number,
-  ): Promise<TFileHandleWriteResult>;
-  writeFile(data: TData, options?: IWriteFileOptions): Promise<void>;
-}
-
-export type TFileHandle = PathLike | IFileHandle;
-
-export interface IPromisesAPI {
-  FileHandle;
-  access(path: PathLike, mode?: number): Promise<void>;
-  appendFile(path: TFileHandle, data: TData, options?: IAppendFileOptions | string): Promise<void>;
-  chmod(path: PathLike, mode: TMode): Promise<void>;
-  chown(path: PathLike, uid: number, gid: number): Promise<void>;
-  copyFile(src: PathLike, dest: PathLike, flags?: TFlagsCopy): Promise<void>;
-  lchmod(path: PathLike, mode: TMode): Promise<void>;
-  lchown(path: PathLike, uid: number, gid: number): Promise<void>;
-  link(existingPath: PathLike, newPath: PathLike): Promise<void>;
-  lstat(path: PathLike, options?: IStatOptions): Promise<Stats>;
-  mkdir(path: PathLike, options?: TMode | IMkdirOptions): Promise<void>;
-  mkdtemp(prefix: string, options?: IOptions): Promise<TDataOut>;
-  open(path: PathLike, flags: TFlags, mode?: TMode): Promise<FileHandle>;
-  readdir(path: PathLike, options?: IReaddirOptions | string): Promise<TDataOut[] | Dirent[]>;
-  readFile(id: TFileHandle, options?: IReadFileOptions | string): Promise<TDataOut>;
-  readlink(path: PathLike, options?: IOptions): Promise<TDataOut>;
-  realpath(path: PathLike, options?: IRealpathOptions | string): Promise<TDataOut>;
-  rename(oldPath: PathLike, newPath: PathLike): Promise<void>;
-  rmdir(path: PathLike): Promise<void>;
-  rm(path: PathLike, options?: IRmOptions): Promise<void>;
-  stat(path: PathLike, options?: IStatOptions): Promise<Stats>;
-  symlink(target: PathLike, path: PathLike, type?: symlink.Type): Promise<void>;
-  truncate(path: PathLike, len?: number): Promise<void>;
-  unlink(path: PathLike): Promise<void>;
-  utimes(path: PathLike, atime: TTime, mtime: TTime): Promise<void>;
-  writeFile(id: TFileHandle, data: TData, options?: IWriteFileOptions): Promise<void>;
-}
-
-export class FileHandle implements IFileHandle {
-  private vol: Volume;
-
-  fd: number;
-
-  constructor(vol: Volume, fd: number) {
-    this.vol = vol;
-    this.fd = fd;
-  }
-
-  appendFile(data: TData, options?: IAppendFileOptions | string): Promise<void> {
-    return promisify(this.vol, 'appendFile')(this.fd, data, options);
-  }
-
-  chmod(mode: TMode): Promise<void> {
-    return promisify(this.vol, 'fchmod')(this.fd, mode);
-  }
-
-  chown(uid: number, gid: number): Promise<void> {
-    return promisify(this.vol, 'fchown')(this.fd, uid, gid);
-  }
-
-  close(): Promise<void> {
-    return promisify(this.vol, 'close')(this.fd);
-  }
-
-  datasync(): Promise<void> {
-    return promisify(this.vol, 'fdatasync')(this.fd);
-  }
-
-  read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise<TFileHandleReadResult> {
-    return promisify(this.vol, 'read', bytesRead => ({ bytesRead, buffer }))(this.fd, buffer, offset, length, position);
-  }
-
-  readFile(options?: IReadFileOptions | string): Promise<TDataOut> {
-    return promisify(this.vol, 'readFile')(this.fd, options);
-  }
-
-  stat(options?: IFStatOptions): Promise<Stats> {
-    return promisify(this.vol, 'fstat')(this.fd, options);
-  }
-
-  sync(): Promise<void> {
-    return promisify(this.vol, 'fsync')(this.fd);
-  }
-
-  truncate(len?: number): Promise<void> {
-    return promisify(this.vol, 'ftruncate')(this.fd, len);
-  }
-
-  utimes(atime: TTime, mtime: TTime): Promise<void> {
-    return promisify(this.vol, 'futimes')(this.fd, atime, mtime);
-  }
-
-  write(
-    buffer: Buffer | Uint8Array,
-    offset?: number,
-    length?: number,
-    position?: number,
-  ): Promise<TFileHandleWriteResult> {
-    return promisify(this.vol, 'write', bytesWritten => ({ bytesWritten, buffer }))(
-      this.fd,
-      buffer,
-      offset,
-      length,
-      position,
-    );
-  }
-
-  writeFile(data: TData, options?: IWriteFileOptions): Promise<void> {
-    return promisify(this.vol, 'writeFile')(this.fd, data, options);
-  }
-}
-
-export default function createPromisesApi(vol: Volume): null | IPromisesAPI {
-  if (typeof Promise === 'undefined') return null;
-  return {
-    FileHandle,
-
-    access(path: PathLike, mode?: number): Promise<void> {
-      return promisify(vol, 'access')(path, mode);
-    },
-
-    appendFile(path: TFileHandle, data: TData, options?: IAppendFileOptions | string): Promise<void> {
-      return promisify(vol, 'appendFile')(path instanceof FileHandle ? path.fd : (path as PathLike), data, options);
-    },
-
-    chmod(path: PathLike, mode: TMode): Promise<void> {
-      return promisify(vol, 'chmod')(path, mode);
-    },
-
-    chown(path: PathLike, uid: number, gid: number): Promise<void> {
-      return promisify(vol, 'chown')(path, uid, gid);
-    },
-
-    copyFile(src: PathLike, dest: PathLike, flags?: TFlagsCopy): Promise<void> {
-      return promisify(vol, 'copyFile')(src, dest, flags);
-    },
-
-    lchmod(path: PathLike, mode: TMode): Promise<void> {
-      return promisify(vol, 'lchmod')(path, mode);
-    },
-
-    lchown(path: PathLike, uid: number, gid: number): Promise<void> {
-      return promisify(vol, 'lchown')(path, uid, gid);
-    },
-
-    link(existingPath: PathLike, newPath: PathLike): Promise<void> {
-      return promisify(vol, 'link')(existingPath, newPath);
-    },
-
-    lstat(path: PathLike, options?: IStatOptions): Promise<Stats> {
-      return promisify(vol, 'lstat')(path, options);
-    },
-
-    mkdir(path: PathLike, options?: TMode | IMkdirOptions): Promise<void> {
-      return promisify(vol, 'mkdir')(path, options);
-    },
-
-    mkdtemp(prefix: string, options?: IOptions): Promise<TDataOut> {
-      return promisify(vol, 'mkdtemp')(prefix, options);
-    },
-
-    open(path: PathLike, flags: TFlags, mode?: TMode): Promise<FileHandle> {
-      return promisify(vol, 'open', fd => new FileHandle(vol, fd))(path, flags, mode);
-    },
-
-    readdir(path: PathLike, options?: IReaddirOptions | string): Promise<TDataOut[] | Dirent[]> {
-      return promisify(vol, 'readdir')(path, options);
-    },
-
-    readFile(id: TFileHandle, options?: IReadFileOptions | string): Promise<TDataOut> {
-      return promisify(vol, 'readFile')(id instanceof FileHandle ? id.fd : (id as PathLike), options);
-    },
-
-    readlink(path: PathLike, options?: IOptions): Promise<TDataOut> {
-      return promisify(vol, 'readlink')(path, options);
-    },
-
-    realpath(path: PathLike, options?: IRealpathOptions | string): Promise<TDataOut> {
-      return promisify(vol, 'realpath')(path, options);
-    },
-
-    rename(oldPath: PathLike, newPath: PathLike): Promise<void> {
-      return promisify(vol, 'rename')(oldPath, newPath);
-    },
-
-    rmdir(path: PathLike): Promise<void> {
-      return promisify(vol, 'rmdir')(path);
-    },
-
-    rm(path: PathLike, options?: IRmOptions): Promise<void> {
-      return promisify(vol, 'rm')(path, options);
-    },
-
-    stat(path: PathLike, options?: IStatOptions): Promise<Stats> {
-      return promisify(vol, 'stat')(path, options);
-    },
-
-    symlink(target: PathLike, path: PathLike, type?: symlink.Type): Promise<void> {
-      return promisify(vol, 'symlink')(target, path, type);
-    },
-
-    truncate(path: PathLike, len?: number): Promise<void> {
-      return promisify(vol, 'truncate')(path, len);
-    },
-
-    unlink(path: PathLike): Promise<void> {
-      return promisify(vol, 'unlink')(path);
-    },
-
-    utimes(path: PathLike, atime: TTime, mtime: TTime): Promise<void> {
-      return promisify(vol, 'utimes')(path, atime, mtime);
-    },
-
-    writeFile(id: TFileHandle, data: TData, options?: IWriteFileOptions): Promise<void> {
-      return promisify(vol, 'writeFile')(id instanceof FileHandle ? id.fd : (id as PathLike), data, options);
-    },
-  };
-}
diff --git a/src/volume.ts b/src/volume.ts
index 78d7726a..2eaedc6e 100644
--- a/src/volume.ts
+++ b/src/volume.ts
@@ -1,5 +1,4 @@
 import * as pathModule from 'path';
-import { PathLike, symlink } from 'fs';
 import { Node, Link, File } from './node';
 import Stats from './Stats';
 import Dirent from './Dirent';
@@ -13,7 +12,8 @@ import { EventEmitter } from 'events';
 import { TEncodingExtended, TDataOut, assertEncoding, strToEncoding, ENCODING_UTF8 } from './encoding';
 import * as errors from './internal/errors';
 import util = require('util');
-import createPromisesApi from './promises';
+import { createPromisesApi } from './node/promises';
+import type { PathLike, symlink } from 'fs';
 
 const resolveCrossPlatform = pathModule.resolve;
 const {
@@ -43,7 +43,7 @@ export interface IError extends Error {
 }
 
 export type TFileId = PathLike | number; // Number is used as a file descriptor.
-export type TData = TDataOut | Uint8Array; // Data formats users can give us.
+export type TData = TDataOut | ArrayBufferView | DataView; // Data formats users can give us.
 export type TFlags = string | number;
 export type TMode = string | number; // Mode can be a String, although docs say it should be a Number.
 export type TTime = number | string | Date;
@@ -343,7 +343,10 @@ const getMkdirOptions = (options): IMkdirOptions => {
 
 // Options for `fs.rmdir` and `fs.rmdirSync`
 export interface IRmdirOptions {
+  /** @deprecated */
   recursive?: boolean;
+  maxRetries?: number;
+  retryDelay?: number;
 }
 const rmdirDefaults: IRmdirOptions = {
   recursive: false,
@@ -930,6 +933,7 @@ export class Volume {
     return json;
   }
 
+  // TODO: `cwd` should probably not invoke `process.cwd()`.
   fromJSON(json: DirectoryJSON, cwd: string = process.cwd()) {
     for (let filename in json) {
       const data = json[filename];
@@ -1095,23 +1099,35 @@ export class Volume {
     }
   }
 
-  private readBase(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, position: number): number {
+  private readBase(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset: number,
+    length: number,
+    position: number,
+  ): number {
     const file = this.getFileByFdOrThrow(fd);
     return file.read(buffer, Number(offset), Number(length), position);
   }
 
-  readSync(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, position: number): number {
+  readSync(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset: number,
+    length: number,
+    position: number,
+  ): number {
     validateFd(fd);
     return this.readBase(fd, buffer, offset, length, position);
   }
 
   read(
     fd: number,
-    buffer: Buffer | Uint8Array,
+    buffer: Buffer | ArrayBufferView | DataView,
     offset: number,
     length: number,
     position: number,
-    callback: (err?: Error | null, bytesRead?: number, buffer?: Buffer | Uint8Array) => void,
+    callback: (err?: Error | null, bytesRead?: number, buffer?: Buffer | ArrayBufferView | DataView) => void,
   ) {
     validateCallback(callback);
 
@@ -1183,9 +1199,21 @@ export class Volume {
     return file.write(buf, offset, length, position);
   }
 
-  writeSync(fd: number, buffer: Buffer | Uint8Array, offset?: number, length?: number, position?: number): number;
+  writeSync(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset?: number,
+    length?: number,
+    position?: number,
+  ): number;
   writeSync(fd: number, str: string, position?: number, encoding?: BufferEncoding): number;
-  writeSync(fd: number, a: string | Buffer | Uint8Array, b?: number, c?: number | BufferEncoding, d?: number): number {
+  writeSync(
+    fd: number,
+    a: string | Buffer | ArrayBufferView | DataView,
+    b?: number,
+    c?: number | BufferEncoding,
+    d?: number,
+  ): number {
     validateFd(fd);
 
     let encoding: BufferEncoding | undefined;
@@ -1217,12 +1245,18 @@ export class Volume {
     return this.writeBase(fd, buf, offset, length, position);
   }
 
-  write(fd: number, buffer: Buffer | Uint8Array, callback: (...args) => void);
-  write(fd: number, buffer: Buffer | Uint8Array, offset: number, callback: (...args) => void);
-  write(fd: number, buffer: Buffer | Uint8Array, offset: number, length: number, callback: (...args) => void);
+  write(fd: number, buffer: Buffer | ArrayBufferView | DataView, callback: (...args) => void);
+  write(fd: number, buffer: Buffer | ArrayBufferView | DataView, offset: number, callback: (...args) => void);
+  write(
+    fd: number,
+    buffer: Buffer | ArrayBufferView | DataView,
+    offset: number,
+    length: number,
+    callback: (...args) => void,
+  );
   write(
     fd: number,
-    buffer: Buffer | Uint8Array,
+    buffer: Buffer | ArrayBufferView | DataView,
     offset: number,
     length: number,
     position: number,


Test patch:
diff --git a/src/__tests__/volume/writeSync.test.ts b/src/__tests__/volume/writeSync.test.ts
index 6500d2b8..80f9104a 100644
--- a/src/__tests__/volume/writeSync.test.ts
+++ b/src/__tests__/volume/writeSync.test.ts
@@ -1,23 +1,28 @@
 import { create } from '../util';
-import { Volume } from '../../volume';
+import { memfs } from '../..';
 
 describe('.writeSync(fd, buffer, offset, length, position)', () => {
-  let vol: Volume;
-
-  beforeEach(() => {
-    vol = create({});
-  });
-
   it('Write binary data to file', () => {
+    const vol = create({});
     const fd = vol.openSync('/data.bin', 'w+');
     const bytes = vol.writeSync(fd, Buffer.from([1, 2, 3]));
     vol.closeSync(fd);
     expect(bytes).toBe(3);
     expect(Buffer.from([1, 2, 3]).equals(vol.readFileSync('/data.bin') as Buffer)).toBe(true);
   });
+
   it('Write string to file', () => {
+    const vol = create({});
     const fd = vol.openSync('/foo', 'w');
     vol.writeSync(fd, 'test');
     expect(vol.toJSON()).toMatchSnapshot();
   });
+
+  it('can write at offset', () => {
+    const fs = memfs({ foo: '123' });
+    const fd = fs.openSync('/foo', 'a+');
+    expect(fs.readFileSync('/foo', 'utf8')).toBe('123');
+    fs.writeSync(fd, 'x', 1);
+    expect(fs.readFileSync('/foo', 'utf8')).toBe('1x3');
+  });
 });