Skip to content

Commit 685791a

Browse files
committed
feat: adds class instance type guard utility
Provides type-safe checking for class instances with optional constructor validation Supports both specific class checking and general instance detection Excludes plain objects, primitives, arrays, and null values Includes comprehensive test coverage for edge cases and type narrowing
1 parent 1e5dab9 commit 685791a

4 files changed

Lines changed: 267 additions & 0 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,34 @@ const defined = values.filter(isDefined); // number[]
5959
console.log(defined); // [1, 2, 3]
6060
```
6161

62+
### isInstanceOfClass
63+
64+
```typescript
65+
import { isInstanceOfClass } from "@sillvva/utils";
66+
67+
class User {
68+
constructor(public name: string) {}
69+
}
70+
71+
const user = new User("John");
72+
const plainObj = { name: "Jane" };
73+
74+
// Check specific class
75+
console.log(isInstanceOfClass(user, User)); // true
76+
console.log(isInstanceOfClass(plainObj, User)); // false
77+
78+
// Check for any class instance
79+
console.log(isInstanceOfClass(user)); // true
80+
console.log(isInstanceOfClass(plainObj)); // false
81+
82+
// Type narrowing
83+
function processUser(data: unknown) {
84+
if (isInstanceOfClass(data, User)) {
85+
console.log(data.name); // TypeScript knows data is User
86+
}
87+
}
88+
```
89+
6290
### isOneOf
6391

6492
```typescript

src/entry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./abortable";
22
export * from "./debounce";
33
export * from "./isDefined";
4+
export * from "./isInstanceOfClass";
45
export * from "./isOneOf";
56
export * from "./slugify";
67
export * from "./sorter";

src/isInstanceOfClass.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { isInstanceOfClass } from "./isInstanceOfClass";
2+
3+
// Test classes
4+
class TestClass {
5+
constructor(public value: string) {}
6+
}
7+
8+
class AnotherClass {
9+
constructor(public number: number) {}
10+
}
11+
12+
class SubClass extends TestClass {
13+
constructor(value: string, public extra: boolean) {
14+
super(value);
15+
}
16+
}
17+
18+
describe("isInstanceOfClass", () => {
19+
describe("with specific class constructor", () => {
20+
it("returns true for instances of the specified class", () => {
21+
const instance = new TestClass("test");
22+
expect(isInstanceOfClass(instance, TestClass)).toBe(true);
23+
});
24+
25+
it("returns true for instances of subclasses", () => {
26+
const subInstance = new SubClass("test", true);
27+
expect(isInstanceOfClass(subInstance, TestClass)).toBe(true);
28+
});
29+
30+
it("returns false for instances of different classes", () => {
31+
const instance = new TestClass("test");
32+
expect(isInstanceOfClass(instance, AnotherClass)).toBe(false);
33+
});
34+
35+
it("returns false for plain objects", () => {
36+
const plainObj = { value: "test" };
37+
expect(isInstanceOfClass(plainObj, TestClass)).toBe(false);
38+
});
39+
40+
it("returns false for primitives", () => {
41+
expect(isInstanceOfClass("string", TestClass)).toBe(false);
42+
expect(isInstanceOfClass(123, TestClass)).toBe(false);
43+
expect(isInstanceOfClass(true, TestClass)).toBe(false);
44+
expect(isInstanceOfClass(Symbol(), TestClass)).toBe(false);
45+
});
46+
47+
it("returns false for null and undefined", () => {
48+
expect(isInstanceOfClass(null, TestClass)).toBe(false);
49+
expect(isInstanceOfClass(undefined, TestClass)).toBe(false);
50+
});
51+
52+
it("returns false for arrays", () => {
53+
expect(isInstanceOfClass([], TestClass)).toBe(false);
54+
expect(isInstanceOfClass([1, 2, 3], TestClass)).toBe(false);
55+
});
56+
57+
it("returns false for functions", () => {
58+
expect(isInstanceOfClass(() => {}, TestClass)).toBe(false);
59+
expect(isInstanceOfClass(function () {}, TestClass)).toBe(false);
60+
});
61+
62+
it("works as a type guard", () => {
63+
const items = [
64+
new TestClass("a"),
65+
{ value: "b" }, // plain object
66+
new AnotherClass(1),
67+
new TestClass("c")
68+
];
69+
70+
const testClassInstances = items.filter((item): item is TestClass => isInstanceOfClass(item, TestClass));
71+
72+
expect(testClassInstances).toHaveLength(2);
73+
expect(testClassInstances[0]).toBeInstanceOf(TestClass);
74+
expect(testClassInstances[1]).toBeInstanceOf(TestClass);
75+
});
76+
});
77+
78+
describe("without class constructor (checking for any class instance)", () => {
79+
it("returns true for class instances", () => {
80+
const testInstance = new TestClass("test");
81+
const anotherInstance = new AnotherClass(123);
82+
const subInstance = new SubClass("test", true);
83+
84+
expect(isInstanceOfClass(testInstance)).toBe(true);
85+
expect(isInstanceOfClass(anotherInstance)).toBe(true);
86+
expect(isInstanceOfClass(subInstance)).toBe(true);
87+
});
88+
89+
it("returns false for plain objects", () => {
90+
const plainObj = { value: "test" };
91+
const emptyObj = {};
92+
93+
expect(isInstanceOfClass(plainObj)).toBe(false);
94+
expect(isInstanceOfClass(emptyObj)).toBe(false);
95+
});
96+
97+
it("returns false for primitives", () => {
98+
expect(isInstanceOfClass("string")).toBe(false);
99+
expect(isInstanceOfClass(123)).toBe(false);
100+
expect(isInstanceOfClass(true)).toBe(false);
101+
expect(isInstanceOfClass(Symbol())).toBe(false);
102+
});
103+
104+
it("returns false for null and undefined", () => {
105+
expect(isInstanceOfClass(null)).toBe(false);
106+
expect(isInstanceOfClass(undefined)).toBe(false);
107+
});
108+
109+
it("returns false for arrays", () => {
110+
expect(isInstanceOfClass([])).toBe(false);
111+
expect(isInstanceOfClass([1, 2, 3])).toBe(false);
112+
});
113+
114+
it("returns false for functions", () => {
115+
expect(isInstanceOfClass(() => {})).toBe(false);
116+
expect(isInstanceOfClass(function () {})).toBe(false);
117+
});
118+
119+
it("works as a type guard for any class instance", () => {
120+
const items = [
121+
new TestClass("a"),
122+
{ value: "b" }, // plain object
123+
new AnotherClass(1),
124+
"string", // primitive
125+
new SubClass("c", true)
126+
];
127+
128+
const classInstances = items.filter((item): item is TestClass | AnotherClass | SubClass => isInstanceOfClass(item));
129+
130+
expect(classInstances).toHaveLength(3);
131+
expect(classInstances[0]).toBeInstanceOf(TestClass);
132+
expect(classInstances[1]).toBeInstanceOf(AnotherClass);
133+
expect(classInstances[2]).toBeInstanceOf(SubClass);
134+
});
135+
});
136+
137+
describe("edge cases", () => {
138+
it("handles objects created with Object.create(null)", () => {
139+
const nullProtoObj = Object.create(null);
140+
expect(isInstanceOfClass(nullProtoObj)).toBe(false); // Not a plain object
141+
expect(isInstanceOfClass(nullProtoObj, TestClass)).toBe(false);
142+
});
143+
144+
it("handles objects with modified prototypes", () => {
145+
const obj = {};
146+
Object.setPrototypeOf(obj, null);
147+
expect(isInstanceOfClass(obj)).toBe(false); // Not a plain object
148+
});
149+
150+
it("handles built-in objects", () => {
151+
expect(isInstanceOfClass(new Date())).toBe(true);
152+
expect(isInstanceOfClass(new Date(), Date)).toBe(true);
153+
expect(isInstanceOfClass(new Date(), TestClass)).toBe(false);
154+
155+
expect(isInstanceOfClass(new RegExp("test"))).toBe(true);
156+
expect(isInstanceOfClass(new RegExp("test"), RegExp)).toBe(true);
157+
});
158+
});
159+
});

src/isInstanceOfClass.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Type guard function that checks if a value is an instance of a class
3+
* @module
4+
*/
5+
6+
/**
7+
* Type guard function that checks if a value is an instance of a class
8+
* @param obj The value to check
9+
* @param ClassConstructor Optional class constructor to check against
10+
* @returns True if the value is an instance of the specified class (or any class if no constructor provided), false otherwise
11+
*
12+
* @example Basic usage with specific class
13+
* import { isInstanceOfClass } from "@sillvva/utils";
14+
*
15+
* class MyClass {}
16+
* const instance = new MyClass();
17+
* const plainObj = {};
18+
*
19+
* console.log(isInstanceOfClass(instance, MyClass)); // true
20+
* console.log(isInstanceOfClass(plainObj, MyClass)); // false
21+
*
22+
* @example Type narrowing with specific class
23+
* import { isInstanceOfClass } from "@sillvva/utils";
24+
*
25+
* class User {
26+
* constructor(public name: string) {}
27+
* }
28+
*
29+
* function processUser(data: unknown) {
30+
* if (isInstanceOfClass(data, User)) {
31+
* // TypeScript knows data is User here
32+
* console.log(data.name);
33+
* }
34+
* }
35+
*
36+
* @example Checking for any class instance
37+
* import { isInstanceOfClass } from "@sillvva/utils";
38+
*
39+
* class MyClass {}
40+
* const instance = new MyClass();
41+
* const plainObj = {};
42+
* const primitive = "string";
43+
*
44+
* console.log(isInstanceOfClass(instance)); // true (instance of MyClass)
45+
* console.log(isInstanceOfClass(plainObj)); // false (plain object)
46+
* console.log(isInstanceOfClass(primitive)); // false (primitive)
47+
* console.log(isInstanceOfClass(null)); // false (null)
48+
*
49+
* @example Filtering class instances from mixed arrays
50+
* import { isInstanceOfClass } from "@sillvva/utils";
51+
*
52+
* class Animal {
53+
* constructor(public name: string) {}
54+
* }
55+
*
56+
* const items = [
57+
* new Animal("Dog"),
58+
* { name: "Cat" }, // plain object
59+
* "Bird", // primitive
60+
* new Animal("Fish")
61+
* ];
62+
*
63+
* const animals = items.filter((item): item is Animal => isInstanceOfClass(item, Animal));
64+
* console.log(animals); // [Animal { name: "Dog" }, Animal { name: "Fish" }]
65+
*/
66+
export function isInstanceOfClass<T extends object>(obj: unknown, ClassConstructor?: new (...args: any[]) => T): obj is T {
67+
// Exclude null and non-objects
68+
if (obj === null || typeof obj !== "object") return false;
69+
70+
// If a specific class constructor is provided, check if obj is an instance of that class
71+
if (ClassConstructor) {
72+
return obj instanceof ClassConstructor;
73+
}
74+
75+
// Otherwise, check if it's an instance of any class (exclude plain objects and arrays)
76+
const prototype = Object.getPrototypeOf(obj);
77+
if (prototype === null) return false;
78+
return prototype !== Object.prototype && prototype !== Array.prototype;
79+
}

0 commit comments

Comments
 (0)