Skip to content

Instantly share code, notes, and snippets.

@Fasteroid
Last active July 29, 2026 03:24
Show Gist options
  • Select an option

  • Save Fasteroid/ae46ef8ca0fa11199081192761995d59 to your computer and use it in GitHub Desktop.

Select an option

Save Fasteroid/ae46ef8ca0fa11199081192761995d59 to your computer and use it in GitHub Desktop.
/*
traits.ts - Easy ECS for TypeScript, powered by mixins!
Concepts and direction by Fasteroid;
Some TypeScript workarounds provided by Claude Sonnet 5.
*/
type Constructor<ARGS extends unknown[] = any[], TYPE = object> = new (...args: ARGS) => TYPE;
type TraitsMap = { readonly [key: symbol]: Constructor };
export const Traits = Symbol("Traits")
/**
* Type-asserts that *{@linkcode t}* is part of the traits system.
*/
function hasTraits(t: any): t is Trait<symbol> {
return t[Traits] !== undefined;
}
/**
* Type-asserts that *{@linkcode host}* implements the specified *{@linkcode trait}* mixin.
*/
function hasTrait<TRAIT extends Trait<symbol>>(
host: InstanceType< Trait<symbol> >,
trait: TRAIT
): host is InstanceType<TRAIT> {
return host[Traits][trait.Symbol] !== undefined;
}
/**
* This wrapper allows you to associate a specific *{@linkcode symbol | unique symbol}* with the provided *{@linkcode trait}* mixin.
*
* Traits can be detected and type-asserted using the {@linkcode hasTrait} function.
*
* @example
* ```ts
* const QuertyTrait = Trait(
* class {
* qwerty = true
* },
* Symbol("QwertyTrait")
* )
* ```
*/
export function Trait<BASE extends Constructor, const TRAIT_SYMBOL extends symbol>(trait: BASE, symbol: TRAIT_SYMBOL) {
let traits = { [symbol]: trait } as { readonly [_ in TRAIT_SYMBOL]: BASE };
const proto = Object.getPrototypeOf(trait);
if( hasTraits(proto) ) {
traits = Object.setPrototypeOf(traits, proto[Traits]); // set up inheritance of trait metadata if necessary
}
// All this juggling is necessary to prevent reduction to the `never` type. Ask claude about it.
class TraitImpl extends (trait as Constructor) {
public static readonly Symbol: symbol = symbol;
public static readonly [Traits]: TraitsMap = traits;
public get [Traits]() { return traits }
}
return TraitImpl as (
new (...args: ConstructorParameters<BASE>) => InstanceType<BASE> & InstanceType<typeof TraitImpl>) &
{
readonly Symbol: TRAIT_SYMBOL;
readonly [Traits]: { readonly [_ in TRAIT_SYMBOL]: BASE };
}
;
}
export type Trait<T extends symbol> = ReturnType<typeof Trait<any, T>>
@Fasteroid

Fasteroid commented Jul 29, 2026

Copy link
Copy Markdown
Author

Quick usage example for the current state this is in:

type Vec3 = [number, number, number]

const ColorTag = Symbol()
function addColor<T extends Constructor>(base: T = Object as any) {
    return Trait(
        class extends base {
            public rgb: Vec3 = [0,0,0];
        },
        ColorTag
    )
}
const Color = addColor();

const AlphaTag = Symbol()
function addAlpha<T extends Constructor>(base: T = Object as any) {
    return Trait(
        class extends base {
            public alpha: number = 255;

            public transmit(color: Vec3) {
                color = [...color];
                color[0] *= this.alpha;
                color[1] *= this.alpha;
                color[2] *= this.alpha

                if( hasTrait(this, Color) ) { // tint the output if this is combined with ColorTrait
                    color[0] *= this.rgb[0];
                    color[1] *= this.rgb[1];
                    color[2] *= this.rgb[2];
                }

                return color
            }
        },
        AlphaTag
    )
}
const Alpha = addAlpha();

let alphaOnly = new Alpha();
alphaOnly.alpha = 0.5
console.log( alphaOnly.transmit([1, 1, 1]) ); // [0.5, 0.5, 0.5]

const alphaAndColor = new( addAlpha(addColor()) )();
alphaAndColor.alpha = 0.5;
alphaAndColor.rgb = [1,0,0];
console.log( alphaAndColor.transmit([1, 1, 1]) ); // [0.5, 0, 0]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment