Last active
July 29, 2026 03:24
-
-
Save Fasteroid/ae46ef8ca0fa11199081192761995d59 to your computer and use it in GitHub Desktop.
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
| /* | |
| 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>> |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quick usage example for the current state this is in: