Skip to content

Instantly share code, notes, and snippets.

@iGoodie
Created August 2, 2024 15:54
Show Gist options
  • Save iGoodie/8b511aa79ace4a96b92d01a49c9c7539 to your computer and use it in GitHub Desktop.
Save iGoodie/8b511aa79ace4a96b92d01a49c9c7539 to your computer and use it in GitHub Desktop.
Dart-like Typescript Mixins
/* --------------------------------- */
/* 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