Created
August 2, 2024 15:54
-
-
Save iGoodie/8b511aa79ace4a96b92d01a49c9c7539 to your computer and use it in GitHub Desktop.
Dart-like Typescript Mixins
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* --------------------------------- */ | |
/* Implementation ------------------ */ | |
/* --------------------------------- */ | |
type Class<A extends any[] = [], T = {}> = new (...args: A) => T; | |
type StaticProps<T> = T extends Class<any, infer I> | |
? Omit<T, "prototype" | "constructor"> & { prototype: I } | |
: never; | |
type Overwrite<A, B> = { | |
[K in keyof A]: K extends keyof B ? B[K] : A[K]; | |
} & B; | |
type Override<A extends Class, B extends Class> = Class< | |
B extends Class<infer Args extends any[], any> ? Args : never, | |
Overwrite<InstanceType<A>, InstanceType<B>> | |
> & | |
Overwrite<StaticProps<A>, StaticProps<B>>; | |
type StitchClasses< | |
TClasses extends Class<any, any>[], | |
$stitch extends Class = TClasses[0] | |
> = TClasses extends [] | |
? $stitch | |
: TClasses extends [ | |
infer TFirst extends Class<any, any>, | |
...infer TRest extends Class<any, any>[] | |
] | |
? StitchClasses<TRest, Override<TFirst, $stitch>> | |
: never; | |
function stitch< | |
TSuper extends Class<any, any>, | |
TMixins extends readonly Class<[], any>[] | |
>(opts: { superClass?: TSuper; withMixins: [...TMixins] }) { | |
const stitchedClass = class { | |
constructor(...args: TSuper extends Class<infer A, any> ? A : never) { | |
if (opts.superClass) { | |
const superConstructed = Reflect.construct(opts.superClass, args); | |
Object.assign(this, superConstructed); | |
} | |
for (let mixin of opts.withMixins) { | |
const mixinConstructed = Reflect.construct(mixin, []); | |
Object.assign(this, mixinConstructed); | |
} | |
} | |
}; | |
for (let mixin of opts.withMixins) { | |
Object.defineProperty(mixin, Symbol.hasInstance, { | |
value: function (value: any) { | |
return ( | |
value instanceof stitchedClass || | |
Function.prototype[Symbol.hasInstance].call(this, value) | |
); | |
}, | |
}); | |
// Merge static variables | |
Object.getOwnPropertyNames(mixin).forEach((propName) => { | |
if (propName === "name") return; | |
if (propName === "length") return; | |
if (propName === "prototype") return; | |
stitchedClass[propName] = mixin[propName]; | |
}); | |
// Merge local methods | |
Object.getOwnPropertyNames(mixin.prototype).forEach((propName) => { | |
if (propName === "constructor") return; | |
stitchedClass.prototype[propName] = mixin.prototype[propName]; | |
}); | |
} | |
return stitchedClass as StitchClasses<[TSuper, ...TMixins]>; | |
} | |
/* ------------------------ */ | |
/* Usage ------------------ */ | |
/* ------------------------ */ | |
/** | |
* Mixins are super similar to Dart's Mixins. | |
* They only have one limitation: | |
* - Mixin constructors CANNOT accept any arguments. | |
*/ | |
class MixinA { | |
static staticA = "A"; | |
overrideValue = "Hello from A"; | |
fieldA = "A"; | |
AAA; | |
constructor() { | |
this.AAA = "AAA"; | |
} | |
methodA() { | |
return "A"; | |
} | |
overrideMe() { | |
return 1 as const; | |
} | |
} | |
class MixinB { | |
static staticB = "B"; | |
overrideValue = "No no, from B"; | |
fieldB = "B"; | |
methodB() { | |
return "B"; | |
} | |
overrideMe() { | |
return 2 as const; | |
} | |
} | |
class MixinC { | |
static staticC = "C"; | |
overrideValue = "No, I mean from C!"; | |
fieldC = "C"; | |
methodC() { | |
return "C"; | |
} | |
overrideMe() { | |
return 3 as const; | |
} | |
} | |
class MySuperClass { | |
someComposite; | |
constructor(someArg: string, someOtherArg: number) { | |
console.log("MySuperClass called with", someArg, someOtherArg); | |
this.someComposite = someArg + someOtherArg; | |
} | |
} | |
class MyClass extends stitch({ | |
superClass: MySuperClass, // <-- Totally optional | |
withMixins: [MixinA, MixinB, MixinC], | |
}) { | |
fieldE = "E"; | |
} | |
const instance = new MyClass("Foo", 999); | |
// Keep static variables | |
MyClass.staticA; //= | |
MyClass.staticB; //= | |
MyClass.staticC; //= | |
// Keep fields | |
instance.AAA; //= | |
instance.fieldA; //= | |
instance.fieldB; //= | |
instance.fieldC; //= | |
instance.someComposite; //= | |
instance.overrideValue; //= | |
instance.fieldE; //= | |
// Keep methods | |
instance.methodA(); //= | |
instance.methodB(); //= | |
instance.methodC(); //= | |
instance.overrideMe(); //= | |
// Keep instanceof | |
instance instanceof MixinA; //= | |
instance instanceof MixinB; //= | |
instance instanceof MixinC; //= | |
instance instanceof MyClass; //= | |
// without breaking old ones ofc | |
new MixinA() instanceof MixinA; //= | |
new MixinB() instanceof MixinB; //= | |
new MixinC() instanceof MixinC; //= | |
/* ------------------------ */ | |
/* Mixin limitations ------ */ | |
/* ------------------------ */ | |
stitch({ | |
withMixins: [ | |
// @ts-expect-error This one shall fail, as Mixins cannot accept any arguments | |
class { | |
constructor(arg1: any) {} | |
}, | |
], | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment