diff --git a/README.md b/README.md
index f0a4d7b..e42f813 100644
--- a/README.md
+++ b/README.md
@@ -135,8 +135,10 @@ The only addition is the `fromEvent` method, which returns an `Observable` that
 
 ### `socket.of(namespace: string)`
 
-Takes an namespace.
-Works the same as in Socket.IO.
+Takes a namespace and returns an instance based on the current config and the given namespace,
+that is added to the end of the current url.
+See [Namespaces - Client Initialization](https://socket.io/docs/v4/namespaces/#client-initialization).
+Instances are reused based on the namespace.
 
 ### `socket.on(eventName: string, callback: Function)`
 
diff --git a/src/socket-io.service.ts b/src/socket-io.service.ts
index 24b1e0d..2f6efdf 100644
--- a/src/socket-io.service.ts
+++ b/src/socket-io.service.ts
@@ -8,6 +8,7 @@ import { SocketIoConfig } from './config/socket-io.config';
 export class WrappedSocket {
   subscribersCounter: Record<string, number> = {};
   eventObservables$: Record<string, Observable<any>> = {};
+  namespaces: Record<string, WrappedSocket> = {};
   ioSocket: any;
   emptyConfig: SocketIoConfig = {
     url: '',
@@ -24,36 +25,80 @@ export class WrappedSocket {
     this.ioSocket = ioFunc(url, options);
   }
 
-  of(namespace: string) {
-    this.ioSocket.of(namespace);
-  }
-
-  on(eventName: string, callback: Function) {
+  /**
+   * Gets a WrappedSocket for the given namespace.
+   *
+   * @note if an existing socket exists for the given namespace, it will be reused.
+   *
+   * @param namespace the namespace to create a new socket based on the current config.
+   *        If empty or `/`, then the current instance is returned.
+   * @returns a socket that is bound to the given namespace. If namespace is empty or `/`,
+   *          then `this` is returned, otherwise another instance is returned, creating
+   *          it if it's the first use of such namespace.
+   */
+  of(namespace: string): WrappedSocket {
+    if (!namespace || namespace === '/') {
+      return this;
+    }
+    const existing = this.namespaces[namespace];
+    if (existing) {
+      return existing;
+    }
+    const { url, ...rest } = this.config;
+    const config = {
+      url:
+        !url.endsWith('/') && !namespace.startsWith('/')
+          ? `${url}/${namespace}`
+          : `${url}${namespace}`,
+      ...rest,
+    };
+    const created = new WrappedSocket(config);
+    this.namespaces[namespace] = created;
+    return created;
+  }
+
+  on(eventName: string, callback: Function): this {
     this.ioSocket.on(eventName, callback);
+    return this;
   }
 
-  once(eventName: string, callback: Function) {
+  once(eventName: string, callback: Function): this {
     this.ioSocket.once(eventName, callback);
+    return this;
+  }
+
+  connect(): this {
+    this.ioSocket.connect();
+    return this;
   }
 
-  connect(callback?: (err: any) => void) {
-    return this.ioSocket.connect(callback);
+  disconnect(): this {
+    this.ioSocket.disconnect();
+    return this;
   }
 
-  disconnect(_close?: any) {
-    return this.ioSocket.disconnect.apply(this.ioSocket, arguments);
+  emit(_eventName: string, ..._args: any[]): this {
+    this.ioSocket.emit.apply(this.ioSocket, arguments);
+    return this;
   }
 
-  emit(_eventName: string, ..._args: any[]) {
-    return this.ioSocket.emit.apply(this.ioSocket, arguments);
+  send(..._args: any[]): this {
+    this.ioSocket.send.apply(this.ioSocket, arguments);
+    return this;
   }
 
-  removeListener(_eventName: string, _callback?: Function) {
-    return this.ioSocket.removeListener.apply(this.ioSocket, arguments);
+  emitWithAck<T>(_eventName: string, ..._args: any[]): Promise<T> {
+    return this.ioSocket.emitWithAck.apply(this.ioSocket, arguments);
   }
 
-  removeAllListeners(_eventName?: string) {
-    return this.ioSocket.removeAllListeners.apply(this.ioSocket, arguments);
+  removeListener(_eventName: string, _callback?: Function): this {
+    this.ioSocket.removeListener.apply(this.ioSocket, arguments);
+    return this;
+  }
+
+  removeAllListeners(_eventName?: string): this {
+    this.ioSocket.removeAllListeners.apply(this.ioSocket, arguments);
+    return this;
   }
 
   fromEvent<T>(eventName: string): Observable<T> {
@@ -84,56 +129,102 @@ export class WrappedSocket {
     return new Promise<T>(resolve => this.once(eventName, resolve));
   }
 
-  listeners(eventName: string) {
+  listeners(eventName: string): Function[] {
     return this.ioSocket.listeners(eventName);
   }
 
-  listenersAny() {
+  listenersAny(): Function[] {
     return this.ioSocket.listenersAny();
   }
 
-  listenersAnyOutgoing() {
+  listenersAnyOutgoing(): Function[] {
     return this.ioSocket.listenersAnyOutgoing();
   }
 
-  off(eventName?: string, listener?: Function[]) {
+  off(eventName?: string, listener?: Function[]): this {
     if (!eventName) {
       // Remove all listeners for all events
-      return this.ioSocket.offAny();
+      this.ioSocket.offAny();
+      return this;
     }
 
     if (eventName && !listener) {
       // Remove all listeners for that event
-      return this.ioSocket.off(eventName);
+      this.ioSocket.off(eventName);
+      return this;
     }
 
     // Removes the specified listener from the listener array for the event named
-    return this.ioSocket.off(eventName, listener);
+    this.ioSocket.off(eventName, listener);
+    return this;
+  }
+
+  offAny(callback?: (event: string, ...args: any[]) => void): this {
+    this.ioSocket.offAny(callback);
+    return this;
+  }
+
+  offAnyOutgoing(callback?: (event: string, ...args: any[]) => void): this {
+    this.ioSocket.offAnyOutgoing(callback);
+    return this;
   }
 
-  onAny(callback: (event: string, ...args: any[]) => void) {
-    return this.ioSocket.onAny(callback);
+  onAny(callback: (event: string, ...args: any[]) => void): this {
+    this.ioSocket.onAny(callback);
+    return this;
   }
 
-  onAnyOutgoing(callback: (event: string, ...args: any[]) => void) {
-    return this.ioSocket.onAnyOutgoing(callback);
+  onAnyOutgoing(callback: (event: string, ...args: any[]) => void): this {
+    this.ioSocket.onAnyOutgoing(callback);
+    return this;
   }
 
-  prependAny(callback: (event: string, ...args: any[]) => void) {
-    return this.ioSocket.prependAny(callback);
+  prependAny(callback: (event: string, ...args: any[]) => void): this {
+    this.ioSocket.prependAny(callback);
+    return this;
   }
 
   prependAnyOutgoing(
     callback: (event: string | symbol, ...args: any[]) => void
-  ) {
-    return this.ioSocket.prependAnyOutgoing(callback);
+  ): this {
+    this.ioSocket.prependAnyOutgoing(callback);
+    return this;
+  }
+
+  timeout(value: number): this {
+    this.ioSocket.timeout(value);
+    return this;
+  }
+
+  get volatile(): this {
+    // this getter has a side-effect of turning the socket instance true,
+    // but it returns the actual instance, so we need to get the value to force the side effect
+    const _ = this.ioSocket.volatile;
+    return this;
+  }
+
+  get active(): boolean {
+    return this.ioSocket.active;
+  }
+
+  get connected(): boolean {
+    return this.ioSocket.connected;
+  }
+
+  get disconnected(): boolean {
+    return this.ioSocket.disconnected;
+  }
+
+  get recovered(): boolean {
+    return this.ioSocket.recovered;
   }
 
-  timeout(value: number) {
-    return this.ioSocket.timeout(value);
+  get id(): string {
+    return this.ioSocket.id;
   }
 
-  volatile() {
-    return this.ioSocket.volatile;
+  compress(value: boolean): this {
+    this.ioSocket.compress(value);
+    return this;
   }
 }