0
# Mixin Detection
1
2
Type-safe mixin detection and type narrowing functionality. Since `instanceof` doesn't work with mixins created by ts-mixer, the `hasMixin` function provides an equivalent capability with full type narrowing support.
3
4
## Capabilities
5
6
### hasMixin Function
7
8
Determines whether an instance has a specific mixin, providing type narrowing similar to `instanceof`.
9
10
```typescript { .api }
11
/**
12
* Determines whether an instance has a specific mixin
13
* Provides type narrowing similar to instanceof
14
* @param instance - Object to test for mixin presence
15
* @param mixin - Class constructor to check for
16
* @returns Type guard indicating if instance has the mixin
17
*/
18
function hasMixin<M>(
19
instance: any,
20
mixin: abstract new (...args) => M
21
): instance is M;
22
```
23
24
**Basic Usage:**
25
26
```typescript
27
import { Mixin, hasMixin } from "ts-mixer";
28
29
class Foo {
30
fooProperty: string = "foo";
31
fooMethod(): string {
32
return "from foo";
33
}
34
}
35
36
class Bar {
37
barProperty: string = "bar";
38
barMethod(): string {
39
return "from bar";
40
}
41
}
42
43
class FooBar extends Mixin(Foo, Bar) {
44
combinedMethod(): string {
45
return this.fooMethod() + " and " + this.barMethod();
46
}
47
}
48
49
const instance = new FooBar();
50
51
// instanceof doesn't work with mixins
52
console.log(instance instanceof FooBar); // true
53
console.log(instance instanceof Foo); // false ❌
54
console.log(instance instanceof Bar); // false ❌
55
56
// hasMixin works correctly
57
console.log(hasMixin(instance, FooBar)); // true
58
console.log(hasMixin(instance, Foo)); // true ✅
59
console.log(hasMixin(instance, Bar)); // true ✅
60
```
61
62
## Type Narrowing
63
64
The `hasMixin` function provides full TypeScript type narrowing:
65
66
```typescript
67
import { Mixin, hasMixin } from "ts-mixer";
68
69
class Drawable {
70
draw(): void {
71
console.log("Drawing...");
72
}
73
}
74
75
class Movable {
76
x: number = 0;
77
y: number = 0;
78
79
move(dx: number, dy: number): void {
80
this.x += dx;
81
this.y += dy;
82
}
83
}
84
85
class Shape extends Mixin(Drawable, Movable) {
86
area: number = 0;
87
}
88
89
function processObject(obj: any) {
90
// Type narrowing with hasMixin
91
if (hasMixin(obj, Drawable)) {
92
obj.draw(); // ✅ TypeScript knows obj has draw() method
93
}
94
95
if (hasMixin(obj, Movable)) {
96
obj.move(10, 20); // ✅ TypeScript knows obj has move() method
97
console.log(obj.x, obj.y); // ✅ TypeScript knows obj has x, y properties
98
}
99
100
if (hasMixin(obj, Shape)) {
101
obj.draw(); // ✅ Available from Drawable
102
obj.move(5, 5); // ✅ Available from Movable
103
console.log(obj.area); // ✅ Available from Shape
104
}
105
}
106
107
const shape = new Shape();
108
processObject(shape); // All type checks pass
109
```
110
111
## Advanced Usage Examples
112
113
### Deep Mixin Hierarchies
114
115
`hasMixin` works with complex mixin hierarchies and nested mixins:
116
117
```typescript
118
import { Mixin, hasMixin } from "ts-mixer";
119
120
class A {
121
methodA(): string { return "A"; }
122
}
123
124
class B {
125
methodB(): string { return "B"; }
126
}
127
128
class AB extends Mixin(A, B) {
129
methodAB(): string { return "AB"; }
130
}
131
132
class C {
133
methodC(): string { return "C"; }
134
}
135
136
class D {
137
methodD(): string { return "D"; }
138
}
139
140
class CD extends Mixin(C, D) {
141
methodCD(): string { return "CD"; }
142
}
143
144
// Mixing already-mixed classes
145
class SuperMixed extends Mixin(AB, CD) {
146
superMethod(): string {
147
return "Super";
148
}
149
}
150
151
class ExtraLayer extends SuperMixed {}
152
153
const instance = new ExtraLayer();
154
155
// hasMixin traverses the entire hierarchy
156
console.log(hasMixin(instance, A)); // true
157
console.log(hasMixin(instance, B)); // true
158
console.log(hasMixin(instance, AB)); // true
159
console.log(hasMixin(instance, C)); // true
160
console.log(hasMixin(instance, D)); // true
161
console.log(hasMixin(instance, CD)); // true
162
console.log(hasMixin(instance, SuperMixed)); // true
163
console.log(hasMixin(instance, ExtraLayer)); // true
164
165
// Type narrowing works at any level
166
if (hasMixin(instance, A)) {
167
instance.methodA(); // ✅ Available
168
}
169
170
if (hasMixin(instance, CD)) {
171
instance.methodC(); // ✅ Available from C
172
instance.methodD(); // ✅ Available from D
173
instance.methodCD(); // ✅ Available from CD
174
}
175
```
176
177
### Abstract Class Support
178
179
`hasMixin` works with abstract classes:
180
181
```typescript
182
import { Mixin, hasMixin } from "ts-mixer";
183
184
abstract class Animal {
185
abstract makeSound(): string;
186
187
name: string;
188
constructor(name: string) {
189
this.name = name;
190
}
191
}
192
193
abstract class Flyable {
194
abstract fly(): string;
195
196
altitude: number = 0;
197
198
ascend(height: number): void {
199
this.altitude += height;
200
}
201
}
202
203
class Bird extends Mixin(Animal, Flyable) {
204
constructor(name: string) {
205
super(name);
206
}
207
208
makeSound(): string {
209
return "Tweet!";
210
}
211
212
fly(): string {
213
return `${this.name} is flying at ${this.altitude} feet`;
214
}
215
}
216
217
const bird = new Bird("Robin");
218
219
console.log(hasMixin(bird, Animal)); // true
220
console.log(hasMixin(bird, Flyable)); // true
221
console.log(hasMixin(bird, Bird)); // true
222
223
// Type narrowing with abstract classes
224
if (hasMixin(bird, Animal)) {
225
console.log(bird.name); // ✅ Available
226
console.log(bird.makeSound()); // ✅ Available
227
}
228
229
if (hasMixin(bird, Flyable)) {
230
bird.ascend(100); // ✅ Available
231
console.log(bird.fly()); // ✅ Available
232
}
233
```
234
235
### Conditional Logic with Multiple Mixins
236
237
```typescript
238
import { Mixin, hasMixin } from "ts-mixer";
239
240
interface Serializable {
241
serialize(): string;
242
}
243
244
class JsonSerializable implements Serializable {
245
serialize(): string {
246
return JSON.stringify(this);
247
}
248
}
249
250
interface Cacheable {
251
cacheKey: string;
252
}
253
254
class SimpleCacheable implements Cacheable {
255
cacheKey: string;
256
257
constructor(key: string) {
258
this.cacheKey = key;
259
}
260
}
261
262
interface Loggable {
263
log(message: string): void;
264
}
265
266
class ConsoleLoggable implements Loggable {
267
log(message: string): void {
268
console.log(`[${new Date().toISOString()}]: ${message}`);
269
}
270
}
271
272
// Various combinations of mixins
273
class DataObject extends Mixin(JsonSerializable, SimpleCacheable) {
274
data: any;
275
276
constructor(data: any, cacheKey: string) {
277
super(cacheKey);
278
this.data = data;
279
}
280
}
281
282
class LoggableDataObject extends Mixin(DataObject, ConsoleLoggable) {}
283
284
function processObject(obj: any) {
285
let capabilities: string[] = [];
286
287
if (hasMixin(obj, JsonSerializable)) {
288
capabilities.push("serializable");
289
const serialized = obj.serialize(); // ✅ Type-safe access
290
}
291
292
if (hasMixin(obj, SimpleCacheable)) {
293
capabilities.push("cacheable");
294
const key = obj.cacheKey; // ✅ Type-safe access
295
}
296
297
if (hasMixin(obj, ConsoleLoggable)) {
298
capabilities.push("loggable");
299
obj.log("Processing object"); // ✅ Type-safe access
300
}
301
302
console.log(`Object has capabilities: ${capabilities.join(", ")}`);
303
}
304
305
const dataObj = new DataObject({ value: 42 }, "my-key");
306
const loggableDataObj = new LoggableDataObject({ value: 100 }, "logged-key");
307
308
processObject(dataObj); // "serializable, cacheable"
309
processObject(loggableDataObj); // "serializable, cacheable, loggable"
310
```
311
312
## Implementation Details
313
314
### Search Algorithm
315
316
`hasMixin` uses a breadth-first search algorithm to traverse:
317
318
1. **Direct instanceof check**: First checks regular instanceof
319
2. **Mixin constituents**: Examines registered mixin components
320
3. **Prototype chain**: Traverses the prototype chain for inheritance
321
4. **Recursive search**: Continues until all paths are exhausted
322
323
### Performance Considerations
324
325
- Uses `WeakMap` for mixin tracking to avoid memory leaks
326
- Breadth-first search prevents infinite loops in circular references
327
- Caches visited classes during search to avoid redundant checks
328
329
### Limitations
330
331
- Only works with classes mixed using ts-mixer
332
- Requires ES6 `Map` support (or polyfill)
333
- Performance is O(n) where n is the size of the mixin hierarchy