├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── CHANGES.md ├── README.md ├── etc ├── eslint.mts ├── logo.ai ├── logo.png ├── logo.svg ├── nps.yaml ├── tsc.json └── vite.mts ├── package.json ├── src ├── traits.spec.ts └── traits.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dst 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dst/traits.spec.* 3 | dst/traits.js 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2 | CHANGES 3 | ======= 4 | 5 | 1.1.3 (2023-05-31) 6 | ------------------ 7 | 8 | - UPDATE: update package dependencies 9 | - CLEANUP: cleanup Vite configuration 10 | 11 | 1.1.2 (2023-05-06) 12 | ------------------ 13 | 14 | - UPDATE: update package dependencies 15 | 16 | 1.1.1 (2023-04-06) 17 | ------------------ 18 | 19 | - UPDATE: update package dependencies 20 | 21 | 1.1.0 (2023-03-08) 22 | ------------------ 23 | 24 | - IMPROVEMENT: add support to derive() for a single trailing regular class 25 | 26 | 1.0.9 (2023-03-07) 27 | ------------------ 28 | 29 | - CLEANUP: improve test suite 30 | - CLEANUP: improve project description 31 | 32 | 1.0.8 (2023-03-07) 33 | ------------------ 34 | 35 | - CLEANUP: remove obsolete entries from package.json 36 | 37 | 1.0.7 (2023-03-06) 38 | ------------------ 39 | 40 | - CLEANUP: improve test suite 41 | - CLEANUP: improve project description 42 | 43 | 1.0.6 (2023-03-06) 44 | ------------------ 45 | 46 | - CLEANUP: improve test suite 47 | - CLEANUP: improve project description 48 | 49 | 1.0.5 (2023-03-06) 50 | ------------------ 51 | 52 | - CLEANUP: improve project description 53 | 54 | 1.0.3 (2023-03-06) 55 | ------------------ 56 | 57 | - REFACTORING: switch to new @traits-ts namespacing 58 | 59 | 1.0.2 (2023-02-26) 60 | ------------------ 61 | 62 | - UPDATE: update package dependencies 63 | 64 | 1.0.1 (2023-02-22) 65 | ------------------ 66 | 67 | - BUGFIX: fix constructor parameter merging 68 | - UPDATE: update package dependencies 69 | 70 | 1.0.0 (2023-01-26) 71 | ------------------ 72 | 73 | (first stable release) 74 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @traits-ts/core 5 | =============== 6 | 7 | **Traits for TypeScript Classes (Core)** 8 | 9 |

10 | Project Home | 11 | Github Repository | 12 | NPM Distribution 13 | 14 |

15 | 16 | 17 | [![github (author stars)](https://img.shields.io/github/stars/rse?logo=github&label=author%20stars&color=%233377aa)](https://github.com/rse) 18 | [![github (author followers)](https://img.shields.io/github/followers/rse?label=author%20followers&logo=github&color=%234477aa)](https://github.com/rse) 19 |
20 | [![npm (project release)](https://img.shields.io/npm/v/@traits-ts/core?logo=npm&label=npm%20release&color=%23cc3333)](https://npmjs.com/@traits-ts/core) 21 | [![npm (project downloads)](https://img.shields.io/npm/dm/@traits-ts/core?logo=npm&label=npm%20downloads&color=%23cc3333)](https://npmjs.com/@traits-ts/core) 22 | 23 | About 24 | ----- 25 | 26 | This is a TypeScript library providing a *trait* (aka *mixin*) 27 | facility for extending classes with *multiple* base functionalities, 28 | although TypeScript/JavaScript technically do not allow multiple 29 | inheritance. 30 | 31 | For this, it internally leverages the regular `class extends` mechanism 32 | and "linearizes" the trait hierarchy at the JavaScript level, so it 33 | is does not have to manipulate the run-time objects at all. At the 34 | TypeScript level, it is fully type-safe and recursively derives all 35 | properties of the traits a class is derived from. 36 | 37 | This library consists of just three API functions: `trait` for defining 38 | a trait (or sub-trait), the API function `derive` for deriving a base 39 | class from one or more defined traits, and the API type-guard function 40 | `derived` to ensure an object has the functionality of a trait under 41 | run-time. 42 | 43 | See also [@traits-ts/stdlib](https://github.com/traits-ts/stdlib) for 44 | a companion library of standard, reusable, generic, typed traits (aka mixins), 45 | based on this base library. Currently, this standard library consists 46 | of the reusable traits *Identifiable*, *Configurable*, *Bindable*, 47 | *Subscribable*, *Hookable*, *Finalizable*, *Traceable*, and 48 | *Serializable*. 49 | 50 | Installation 51 | ------------ 52 | 53 | ```sh 54 | $ npm install --save @traits-ts/core 55 | ``` 56 | 57 | API 58 | --- 59 | 60 | The Application Programming Interface (API) of **@traits-ts/core** consists 61 | of just three API functions and can be used in the following way: 62 | 63 | ```ts 64 | // Import API functions. 65 | import { trait, derive, derived } from "@traits-ts/core" 66 | // ===== ====== ======= 67 | 68 | // Define regular trait Foo. 69 | const Foo = trait((base) => class Foo extends base { ... }) 70 | // ===================== ============ 71 | 72 | // Define regular sub-trait Foo, inheriting from super-traits Bar and Qux. 73 | const Foo = trait([ Bar, Qux ], (base) => class Foo extends base { ... }) 74 | // ============ 75 | 76 | // Define generic trait Foo. 77 | const Foo = () => trait((base) => class Foo extends base { ... ... }) 78 | // ===== === 79 | 80 | // Define generic sub-trait Foo, inheriting from super-traits Bar and Qux. 81 | const Foo = () => trait([ Bar, Qux ], (base) => class Foo extends base { ... ... }) 82 | // ===== =============== === 83 | 84 | // Define application class with features derived from traits Foo, Bar and Qux. 85 | class Sample extends derive(Foo, Bar, Qux) { ... } 86 | // ========================== 87 | 88 | // Define application class with features derived from traits and a trailing regular class 89 | class Sample extends derive(Foo, Bar, Qux, EventEmitter) { ... } 90 | // ============ 91 | 92 | // Call super constructor from application class constructor. 93 | class Sample extends derive(...) { constructor () { super(); ... } ... } 94 | // ======= 95 | 96 | // Call super method from application class method. 97 | class Sample extends derive(...) { foo () { ...; super.foo(...); ... } ... } 98 | // ============== 99 | 100 | // Check whether application class is derived from a trait. 101 | const sample = new Sample(); if (derived(sample, Foo)) ... 102 | // ==================== 103 | ``` 104 | 105 | Examples 106 | ------- 107 | 108 | ### Regular, Orthogonal/Independent Traits 109 | 110 | ```ts 111 | import { trait, derive } from "@traits-ts/core" 112 | 113 | const Duck = trait((base) => class extends base { 114 | squeak () { return "squeak" } 115 | }) 116 | const Parrot = trait((base) => class extends base { 117 | talk () { return "talk" } 118 | }) 119 | const Animal = class Animal extends derive(Duck, Parrot) { 120 | walk () { return "walk" } 121 | } 122 | 123 | const animal = new Animal() 124 | 125 | animal.squeak() // -> "squeak" 126 | animal.talk() // -> "talk" 127 | animal.walk() // -> "walk" 128 | ``` 129 | 130 | ### Regular, Bounded/Dependent Traits 131 | 132 | ```ts 133 | import { trait, derive } from "@traits-ts/core" 134 | 135 | const Queue = trait((base) => class extends base { 136 | private buf: Array = [] 137 | get () { return this.buf.pop() } 138 | put (x: number) { this.buf.unshift(x) } 139 | }) 140 | const Doubling = trait([ Queue ], (base) => class extends base { 141 | put (x: number) { super.put(2 * x) } 142 | }) 143 | const Incrementing = trait([ Queue ], (base) => class extends base { 144 | put (x: number) { super.put(x + 1) } 145 | }) 146 | const Filtering = trait([ Queue ], (base) => class extends base { 147 | put (x: number) { if (x >= 0) super.put(x) } 148 | }) 149 | 150 | const MyQueue = class MyQueue extends 151 | derive(Filtering, Doubling, Incrementing, Queue) {} 152 | 153 | const queue = new MyQueue() 154 | 155 | queue.get() // -> undefined 156 | queue.put(-1) 157 | queue.get() // -> undefined 158 | queue.put(1) 159 | queue.get() // -> 3 160 | queue.put(10) 161 | queue.get() // -> 21 162 | ``` 163 | 164 | ### Generic, Bounded/Dependent Traits 165 | 166 | ```ts 167 | import { trait, derive } from "@traits-ts/core" 168 | 169 | const Queue = () => trait((base) => class extends base { 170 | private buf: Array = [] 171 | get () { return this.buf.pop() } 172 | put (x: T) { this.buf.unshift(x) } 173 | }) 174 | const Tracing = () => trait([ Queue ], (base) => class extends base { 175 | private trace (ev: string, x?: T) { console.log(ev, x) } 176 | get () { const x = super.get(); this.trace("get", x); return x } 177 | put (x: T) { this.trace("put", x); super.put(x) } 178 | }) 179 | 180 | const MyTracingQueue = class MyTracingQueue extends 181 | derive(Tracing, Queue) {} 182 | 183 | const queue = new MyTracingQueue() 184 | 185 | queue.put("foo") // -> console: put foo 186 | queue.get() // -> console: get foo 187 | queue.put("bar") // -> console: put bar 188 | queue.put("qux") // -> console: put qux 189 | queue.get() // -> console: get bar 190 | queue.get() // -> console: get qux 191 | ``` 192 | 193 | History 194 | ------- 195 | 196 | The **@traits-ts/core** library was developed in January 2025 by Dr. Ralf 197 | S. Engelschall. It is heavily inspired by Scala traits and the API 198 | of **@ddd-ts/traits**, although **@traits-ts/core** is a "from scratch" 199 | implementation for TypeScript. 200 | 201 | Support 202 | ------- 203 | 204 | The work on this Open Source Software was financially supported by the 205 | german non-profit organisation *SEA Software Engineering Academy gGmbH*. 206 | 207 | License 208 | ------- 209 | 210 | Copyright © 2025 Dr. Ralf S. Engelschall (http://engelschall.com/) 211 | 212 | Permission is hereby granted, free of charge, to any person obtaining 213 | a copy of this software and associated documentation files (the 214 | "Software"), to deal in the Software without restriction, including 215 | without limitation the rights to use, copy, modify, merge, publish, 216 | distribute, sublicense, and/or sell copies of the Software, and to 217 | permit persons to whom the Software is furnished to do so, subject to 218 | the following conditions: 219 | 220 | The above copyright notice and this permission notice shall be included 221 | in all copies or substantial portions of the Software. 222 | 223 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 224 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 225 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 226 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 227 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 228 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 229 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 230 | 231 | -------------------------------------------------------------------------------- /etc/eslint.mts: -------------------------------------------------------------------------------- 1 | /* 2 | ** @rse/traits - Traits for TypeScript Classes 3 | ** Copyright (c) 2025 Dr. Ralf S. Engelschall 4 | ** Licensed under MIT license 5 | */ 6 | 7 | import pluginJs from "@eslint/js" 8 | import pluginStd from "neostandard" 9 | import pluginN from "eslint-plugin-n" 10 | import pluginImport from "eslint-plugin-import" 11 | import pluginPromise from "eslint-plugin-promise" 12 | import pluginMocha from "eslint-plugin-mocha" 13 | import pluginChai from "eslint-plugin-chai-expect" 14 | import pluginTS from "typescript-eslint" 15 | import globals from "globals" 16 | import parserTS from "@typescript-eslint/parser" 17 | 18 | export default [ 19 | pluginJs.configs.recommended, 20 | pluginMocha.configs.recommended, 21 | pluginChai.configs["recommended-flat"], 22 | ...pluginTS.configs.strict, 23 | ...pluginTS.configs.stylistic, 24 | ...pluginStd({ 25 | ignores: pluginStd.resolveIgnoresFromGitignore() 26 | }), 27 | { 28 | plugins: { 29 | "n": pluginN, 30 | "import": pluginImport, 31 | "promise": pluginPromise 32 | }, 33 | files: [ "src/**/*.ts" ], 34 | languageOptions: { 35 | ecmaVersion: 2022, 36 | sourceType: "module", 37 | parser: parserTS, 38 | parserOptions: { 39 | ecmaFeatures: { 40 | jsx: false 41 | } 42 | }, 43 | globals: { 44 | ...globals.node, 45 | ...globals.browser, 46 | ...globals.commonjs 47 | } 48 | }, 49 | rules: { 50 | "curly": "off", 51 | "require-atomic-updates": "off", 52 | "dot-notation": "off", 53 | "no-labels": "off", 54 | "no-useless-constructor": "off", 55 | "no-unused-vars": "off", 56 | 57 | "@stylistic/indent": [ "error", 4, { SwitchCase: 1 } ], 58 | "@stylistic/linebreak-style": [ "error", "unix" ], 59 | "@stylistic/semi": [ "error", "never" ], 60 | "@stylistic/operator-linebreak": [ "error", "after", { overrides: { "&&": "before", "||": "before", ":": "after" } } ], 61 | "@stylistic/brace-style": [ "error", "stroustrup", { allowSingleLine: true } ], 62 | "@stylistic/quotes": [ "error", "double" ], 63 | 64 | "@stylistic/no-multi-spaces": "off", 65 | "@stylistic/no-multi-spaces": "off", 66 | "@stylistic/no-multiple-empty-lines": "off", 67 | "@stylistic/key-spacing": "off", 68 | "@stylistic/object-property-newline": "off", 69 | "@stylistic/space-in-parens": "off", 70 | "@stylistic/array-bracket-spacing": "off", 71 | "@stylistic/lines-between-class-members": "off", 72 | "@stylistic/multiline-ternary": "off", 73 | "@stylistic/quote-props": "off", 74 | "@stylistic/indent": "off", 75 | 76 | "@typescript-eslint/no-empty-function": "off", 77 | "@typescript-eslint/no-explicit-any": "off", 78 | "@typescript-eslint/no-unused-vars": "off", 79 | "@typescript-eslint/ban-ts-comment": "off", 80 | "@typescript-eslint/no-this-alias": "off", 81 | "@typescript-eslint/no-non-null-assertion": "off", 82 | "@typescript-eslint/consistent-type-definitions": "off", 83 | "@typescript-eslint/array-type": "off", 84 | "@typescript-eslint/no-extraneous-class": "off", 85 | "@typescript-eslint/consistent-indexed-object-style": "off", 86 | "@typescript-eslint/prefer-function-type": "off", 87 | "@typescript-eslint/no-unnecessary-type-constraint": "off", 88 | "@typescript-eslint/no-empty-object-type": "off", 89 | 90 | "mocha/no-mocha-arrows": "off" 91 | } 92 | } 93 | ] 94 | 95 | -------------------------------------------------------------------------------- /etc/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traits-ts/core/ff02a2bb29018c5ac6ef1ebebc7a7129caf50b8b/etc/logo.ai -------------------------------------------------------------------------------- /etc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traits-ts/core/ff02a2bb29018c5ac6ef1ebebc7a7129caf50b8b/etc/logo.png -------------------------------------------------------------------------------- /etc/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /etc/nps.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | ## @rse/traits - Traits for TypeScript Classes 3 | ## Copyright (c) 2025 Dr. Ralf S. Engelschall 4 | ## Licensed under MIT license 5 | ## 6 | 7 | scripts: 8 | # all-in-one development 9 | dev: nodemon --exec "npm start lint build test" --watch src --ext ts,1 10 | 11 | # static code analysis (linting) 12 | lint: eslint --config etc/eslint.mts src/**/*.ts 13 | lint-watch: nodemon --exec "npm start lint" --watch src --ext ts,1 14 | 15 | # code compilation/transpiling (building) 16 | build: npm start lint build-esm build-umd 17 | build-esm: VITE_BUILD_FORMATS=esm,cjs vite --config etc/vite.mts build --mode production 18 | build-umd: VITE_BUILD_FORMATS=umd vite --config etc/vite.mts build --mode production 19 | build-watch: nodemon --exec "npm start build" --watch src --ext ts,1 20 | 21 | # test 22 | test: NODE_OPTIONS="--import=tsx" mocha src/traits.spec.ts 23 | 24 | # cleanup 25 | clean: shx rm -rf dst-stage1 dst-stage2 26 | clean-dist: npm start clean && shx rm -rf node_modules 27 | 28 | -------------------------------------------------------------------------------- /etc/tsc.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../dst", 4 | "target": "es2022", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "useDefineForClassFields": false, 8 | "composite": false, 9 | "strict": true, 10 | "strictFunctionTypes": true, 11 | "resolveJsonModule": false, 12 | "isolatedModules": false, 13 | "esModuleInterop": false, 14 | "lib": [ "es2022" ], 15 | "skipLibCheck": false, 16 | "declaration": true, 17 | "noEmit": false, 18 | "types": [ "mocha" ], 19 | "rootDir": "../src" 20 | }, 21 | "include": [ 22 | "../src/**/*.ts", 23 | "../src/**/*.d.ts", 24 | "../package.json" 25 | ], 26 | "exclude": [ 27 | "../node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /etc/vite.mts: -------------------------------------------------------------------------------- 1 | /* 2 | ** @rse/traits - Traits for TypeScript Classes 3 | ** Copyright (c) 2025 Dr. Ralf S. Engelschall 4 | ** Licensed under MIT license 5 | */ 6 | 7 | import fs from "node:fs" 8 | import * as Vite from "vite" 9 | import { tscPlugin } from "@wroud/vite-plugin-tsc" 10 | import { viteSingleFile } from "vite-plugin-singlefile" 11 | import { nodePolyfills } from "vite-plugin-node-polyfills" 12 | 13 | const formats = process.env.VITE_BUILD_FORMATS ?? "esm" 14 | 15 | export default Vite.defineConfig(({ command, mode }) => ({ 16 | logLevel: "info", 17 | appType: "custom", 18 | base: "", 19 | root: "", 20 | plugins: [ 21 | tscPlugin({ 22 | tscArgs: [ "--project", "etc/tsc.json" ], 23 | packageManager: "npx", 24 | prebuild: true 25 | }), 26 | ...(formats === "umd" ? [ nodePolyfills() ] : []), 27 | viteSingleFile() 28 | ], 29 | build: { 30 | lib: { 31 | entry: "dst/traits.js", 32 | formats: formats.split(","), 33 | name: "Traits", 34 | fileName: (format) => `traits.${format === "es" ? "esm" : format}.js` 35 | }, 36 | target: "es2022", 37 | outDir: "dst", 38 | assetsDir: "", 39 | emptyOutDir: (mode === "production") && formats !== "umd", 40 | chunkSizeWarningLimit: 5000, 41 | assetsInlineLimit: 0, 42 | sourcemap: (mode === "development"), 43 | minify: (mode === "production") && formats === "umd", 44 | reportCompressedSize: (mode === "production") 45 | } 46 | })) 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traits-ts/core", 3 | "publishConfig": { "access": "public" }, 4 | "description": "Traits for TypeScript Classes", 5 | "keywords": [ "traits", "class", "mixin" ], 6 | "homepage": "https://traits-ts.org", 7 | "repository": { "url": "git+https://github.com/traits-ts/core.git", "type": "git" }, 8 | "bugs": { "url": "http://github.com/traits-ts/core/issues" }, 9 | "version": "1.1.3", 10 | "license": "MIT", 11 | "author": { 12 | "name": "Dr. Ralf S. Engelschall", 13 | "email": "rse@engelschall.com", 14 | "url": "http://engelschall.com" 15 | }, 16 | "types": "./dst/traits.d.ts", 17 | "module": "./dst/traits.esm.js", 18 | "main": "./dst/traits.cjs.js", 19 | "browser": "./dst/traits.umd.js", 20 | "exports": { 21 | ".": { 22 | "import": { "types": "./dst/traits.d.ts", "default": "./dst/traits.esm.js" }, 23 | "require": { "types": "./dst/traits.d.ts", "default": "./dst/traits.cjs.js" } 24 | } 25 | }, 26 | "devDependencies": { 27 | "eslint": "9.28.0", 28 | "@eslint/js": "9.28.0", 29 | "eslint-plugin-n": "17.18.0", 30 | "eslint-plugin-promise": "7.2.1", 31 | "eslint-plugin-import": "2.31.0", 32 | "eslint-plugin-mocha": "11.1.0", 33 | "eslint-plugin-chai-expect": "3.1.0", 34 | "neostandard": "0.12.1", 35 | "globals": "16.2.0", 36 | 37 | "vite": "6.3.5", 38 | "vite-plugin-singlefile": "2.2.0", 39 | "vite-plugin-node-polyfills": "0.23.0", 40 | "@wroud/vite-plugin-tsc": "0.11.6", 41 | "typescript": "5.8.3", 42 | "tsx": "4.19.4", 43 | "jiti": "2.4.2", 44 | "mocha": "11.5.0", 45 | "chai": "5.2.0", 46 | "chai-as-promised": "8.0.1", 47 | "sinon-chai": "4.0.0", 48 | "sinon": "20.0.0", 49 | 50 | "nps": "5.10.0", 51 | "nodemon": "3.1.10", 52 | "shx": "0.4.0", 53 | 54 | "@types/node": "22.15.29", 55 | "@types/chai": "5.2.2", 56 | "@types/mocha": "10.0.10", 57 | "@types/sinon": "17.0.4", 58 | "@types/sinon-chai": "4.0.0", 59 | "@types/chai-as-promised": "8.0.2" 60 | }, 61 | "scripts": { 62 | "start": "nps -c etc/nps.yaml" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/traits.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ** @traits-ts/core - Traits for TypeScript Classes 3 | ** Copyright (c) 2025 Dr. Ralf S. Engelschall 4 | ** Licensed under MIT license 5 | */ 6 | 7 | import * as chai from "chai" 8 | import sinon from "sinon" 9 | import sinonChai from "sinon-chai" 10 | 11 | import { trait, derive, derived } from "./traits" 12 | 13 | const expect = chai.expect 14 | chai.config.includeStack = true 15 | chai.use(sinonChai) 16 | 17 | describe("@traits-ts/traits", () => { 18 | it("exposed API", () => { 19 | expect(trait).to.be.a("function") 20 | expect(derive).to.be.a("function") 21 | expect(derived).to.be.a("function") 22 | }) 23 | 24 | it("basic derivation", () => { 25 | const Trait1 = trait((base) => class extends base { 26 | trait1 () {} 27 | }) 28 | const Trait2 = () => trait((base) => class extends base { 29 | trait2 () {} 30 | }) 31 | class Clazz { 32 | clazz () {} 33 | } 34 | class Clazz2 { 35 | clazz2 () {} 36 | } 37 | const Sample = class Sample extends 38 | derive(Trait1, Clazz) {} 39 | const sample = new Sample() 40 | expect(sample.trait1).to.be.a("function") 41 | expect(sample.clazz).to.be.a("function") 42 | expect(derived(sample, Trait1)).to.be.equal(true) 43 | expect(derived(sample, Trait2)).to.be.equal(false) 44 | expect(derived(sample, Clazz)).to.be.equal(true) 45 | expect(derived(sample, Clazz2)).to.be.equal(false) 46 | }) 47 | 48 | it("basic usage", () => { 49 | const Swim = trait((base) => class Swim extends base { 50 | static swimmers = 1 51 | swimming = 0 52 | swim () { return this.swimming++ } 53 | }) 54 | const Walk = trait((base) => class Walk extends base { 55 | static walkers = 2 56 | static fastWalk () { return "fastWalk" } 57 | walking = 0 58 | walk () { return this.walking++ } 59 | }) 60 | class Sample extends derive(Swim, Walk) { 61 | static samplers = 3 62 | sampling = 0 63 | perform () { 64 | expect(this.sampling).to.be.equal(0) 65 | expect(this.swimming).to.be.equal(0) 66 | expect(this.walking).to.be.equal(0) 67 | expect(this.swim()).to.be.equal(0) 68 | expect(this.walk()).to.be.equal(0) 69 | expect(this.swim()).to.be.equal(1) 70 | expect(this.walk()).to.be.equal(1) 71 | expect(this.swim()).to.be.equal(2) 72 | expect(this.walk()).to.be.equal(2) 73 | } 74 | } 75 | const sample = new Sample() 76 | expect(Sample.swimmers).to.be.equal(1) 77 | expect(Sample.walkers).to.be.equal(2) 78 | expect(Sample.samplers).to.be.equal(3) 79 | expect(Sample.fastWalk()).to.be.equal("fastWalk") 80 | sample.perform() 81 | }) 82 | 83 | it("complex usage", () => { 84 | const spy = sinon.spy() 85 | const Foo = trait((base) => class Foo extends base { 86 | constructor () { super(); spy("Foo") } 87 | }) 88 | const Bar = () => trait((base) => class Bar extends base { 89 | constructor () { super(); spy("Bar") } 90 | }) 91 | const Baz = trait([ Bar, Foo ], (base) => class Baz extends base { 92 | constructor () { super(); spy("Baz") } 93 | }) 94 | class App extends derive(Baz) { 95 | constructor () { super(); spy("App") } 96 | } 97 | const app = new App() 98 | expect(derived(app, Foo)).to.be.equal(true) 99 | expect(spy.getCalls().map((x) => x.args[0])) 100 | .to.be.deep.equal([ "Foo", "Bar", "Baz", "App" ]) 101 | }) 102 | 103 | it("double derivation", () => { 104 | const spy = sinon.spy() 105 | const Foo = trait((base) => class extends base { 106 | constructor () { super(); spy("Foo") } 107 | }) 108 | const Bar = trait([ Foo ], (base) => class extends base { 109 | constructor () { super(); spy("Bar") } 110 | }) 111 | const Baz = trait([ Bar, Foo ], (base) => class Baz extends base { 112 | constructor () { super(); spy("Baz") } 113 | }) 114 | class App extends derive(Baz, Foo, Foo) { 115 | constructor () { super(); spy("App") } 116 | } 117 | const app = new App() 118 | expect(spy.getCalls().map((x) => x.args[0])) 119 | .to.be.deep.equal([ "Foo", "Bar", "Baz", "App" ]) 120 | }) 121 | 122 | it("super usage", () => { 123 | const Foo = trait((base) => class Foo extends base { 124 | quux (arg: string) { 125 | return `foo.quux(${arg})` 126 | } 127 | }) 128 | const Bar = trait((base) => class Bar extends base { 129 | quux (arg: string) { 130 | return `bar.quux(${super.quux(arg)})` 131 | } 132 | }) 133 | class App extends derive(Bar, Foo) { 134 | quux (arg: string) { 135 | return `app.quux(${super.quux(arg)})` 136 | } 137 | } 138 | const app = new App() 139 | expect(app.quux("start")).to.be.equal("app.quux(bar.quux(foo.quux(start)))") 140 | }) 141 | 142 | it("constructor super usage", () => { 143 | const spy = sinon.spy() 144 | interface Sample { 145 | foo1: string 146 | foo2: number 147 | } 148 | const Foo = () => trait((base) => class Foo extends base { 149 | constructor (params: { [ key: string ]: unknown; foo?: T } | undefined) { 150 | super(params) 151 | spy("Foo", params?.foo?.foo1 === "foo" && params?.foo?.foo2 === 7) 152 | } 153 | }) 154 | const Bar = trait([ Foo ], (base) => class Bar extends base { 155 | constructor (params: { [key: string ]: unknown; bar?: number } | undefined) { 156 | super(params) 157 | spy("Bar", params?.bar === 42) 158 | } 159 | }) 160 | class App extends derive(Bar) { 161 | constructor () { 162 | super({ foo: { foo1: "foo", foo2: 7 }, bar: 42 }) 163 | spy("App") 164 | } 165 | } 166 | const app = new App() 167 | expect(spy.getCalls().map((x) => x.args.join(":"))) 168 | .to.be.deep.equal([ "Foo:true", "Bar:true", "App" ]) 169 | }) 170 | 171 | it("sample traits: regular, orthogonal", () => { 172 | const Duck = trait((base) => class extends base { 173 | squeak () { return "squeak" } 174 | }) 175 | const Parrot = trait((base) => class extends base { 176 | talk () { return "talk" } 177 | }) 178 | const Animal = class Animal extends derive(Duck, Parrot) { 179 | walk () { return "walk" } 180 | } 181 | const animal = new Animal() 182 | expect(animal.squeak()).to.be.equal("squeak") 183 | expect(animal.talk()).to.be.equal("talk") 184 | expect(animal.walk()).to.be.equal("walk") 185 | }) 186 | 187 | it("sample traits: regular, bounded", () => { 188 | const Queue = trait((base) => class extends base { 189 | private buf: Array = [] 190 | get () { return this.buf.pop() } 191 | put (x: number) { this.buf.unshift(x) } 192 | }) 193 | const Doubling = trait([ Queue ], (base) => class extends base { 194 | put (x: number) { super.put(2 * x) } 195 | }) 196 | const Incrementing = trait([ Queue ], (base) => class extends base { 197 | put (x: number) { super.put(x + 1) } 198 | }) 199 | const Filtering = trait([ Queue ], (base) => class extends base { 200 | put (x: number) { if (x >= 0) super.put(x) } 201 | }) 202 | 203 | const MyQueue = class MyQueue extends 204 | derive(Filtering, Doubling, Incrementing, Queue) {} 205 | const queue = new MyQueue() 206 | expect(queue.get()).to.be.equal(undefined) 207 | queue.put(-1) 208 | expect(queue.get()).to.be.equal(undefined) 209 | queue.put(1) 210 | expect(queue.get()).to.be.equal(3) 211 | queue.put(10) 212 | expect(queue.get()).to.be.equal(21) 213 | }) 214 | 215 | it("sample traits: generic, bounded", () => { 216 | const Queue = () => trait((base) => class extends base { 217 | private buf: Array = [] 218 | get () { return this.buf.pop() } 219 | put (x: T) { this.buf.unshift(x) } 220 | }) 221 | const Tracing = () => trait([ Queue ], (base) => class extends base { 222 | public onTrace = (ev: string, x?: T) => {} 223 | get () { const x = super.get(); this.onTrace("get", x); return x } 224 | put (x: T) { this.onTrace("put", x); super.put(x) } 225 | }) 226 | 227 | const MyTracingQueue = class MyTracingQueue extends 228 | derive(Tracing, Queue) {} 229 | const spy = sinon.spy() 230 | const queue = new MyTracingQueue() 231 | queue.onTrace = (ev: string, x?: string) => { spy(ev, x) } 232 | queue.put("foo") 233 | queue.get() 234 | queue.put("bar") 235 | queue.put("qux") 236 | queue.get() 237 | queue.get() 238 | expect(spy.getCalls().map((x) => x.args.join(":"))).to.be.deep.equal([ 239 | "put:foo", "get:foo", "put:bar", "put:qux", "get:bar", "get:qux" 240 | ]) 241 | }) 242 | }) 243 | -------------------------------------------------------------------------------- /src/traits.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ** @traits-ts/core - Traits for TypeScript Classes 3 | ** Copyright (c) 2025 Dr. Ralf S. Engelschall 4 | ** Licensed under MIT license 5 | */ 6 | 7 | /* eslint no-use-before-define: off */ 8 | 9 | /* ==== UTILITY DEFINITIONS ==== */ 10 | 11 | /* utility function: CRC32-hashing a string into a unique identifier */ 12 | const crcTable = [] as number[] 13 | for (let n = 0; n < 256; n++) { 14 | let c = n 15 | for (let k = 0; k < 8; k++) 16 | c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)) 17 | crcTable[n] = c 18 | } 19 | const crc32 = (str: string) => { 20 | let crc = 0 ^ (-1) 21 | for (let i = 0; i < str.length; i++) 22 | crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF] 23 | return (crc ^ (-1)) >>> 0 24 | } 25 | 26 | /* utility type and function: regular function */ 27 | type Func = 28 | (...args: any[]) => T 29 | const isFunc = 30 | 31 | (fn: unknown): fn is Func => 32 | typeof fn === "function" && !!fn.prototype && !!fn.prototype.constructor 33 | 34 | /* utility type and function: constructor (function) */ 35 | type Cons = 36 | new (...args: any[]) => T 37 | const isCons = 38 | 39 | (fn: unknown): fn is Cons => 40 | typeof fn === "function" && !!fn.prototype && !!fn.prototype.constructor 41 | 42 | /* utility type and function: constructor factory (function) */ 43 | type ConsFactory = 44 | (base: B) => T 45 | const isConsFactory = 46 | 47 | (fn: unknown): fn is ConsFactory => 48 | typeof fn === "function" && !fn.prototype && fn.length === 1 49 | 50 | /* utility type and function: type factory (function) */ 51 | type TypeFactory = 52 | () => T 53 | const isTypeFactory = 54 | 55 | (fn: unknown): fn is TypeFactory => 56 | typeof fn === "function" && !fn.prototype && fn.length === 0 57 | 58 | /* utility type: map an object type into a bare properties type */ 59 | type Explode = 60 | { [ P in keyof T ]: T[P] } 61 | 62 | /* utility type: convert a union type to an intersection type */ 63 | type UnionToIntersection = 64 | (U extends any ? (k: U) => void : never) extends 65 | (k: infer I) => void ? I : never 66 | 67 | /* utility type: convert an array type to a union type */ 68 | type ArrayToUnion = 69 | T[number] 70 | 71 | /* utility type: ensure that an array contains at least one element */ 72 | type ArrayNonEmpty = 73 | [ T, ...T[] ] 74 | 75 | /* utility type: convert two arrays of types into an array of union types */ 76 | type MixParams = 77 | T1 extends [] ? ( 78 | T2 extends [] ? [] : T2 79 | ) : ( 80 | T2 extends [] ? T1 : ( 81 | T1 extends [ infer H1, ...infer R1 ] ? ( 82 | T2 extends [ infer H2, ...infer R2 ] ? 83 | [ H1 & H2, ...MixParams ] 84 | : [] 85 | ) : [] 86 | ) 87 | ) 88 | 89 | /* ==== TRAIT DEFINITION ==== */ 90 | 91 | /* API: trait type */ 92 | type TraitDefTypeT = ConsFactory 93 | type TraitDefTypeST = (Trait | TypeFactory)[] | undefined 94 | export type Trait< 95 | T extends TraitDefTypeT = TraitDefTypeT, 96 | ST extends TraitDefTypeST = TraitDefTypeST 97 | > = { 98 | id: number /* unique id (primary, for hasTrait) */ 99 | symbol: symbol /* unique id (secondary, currently unused) */ 100 | factory: T 101 | superTraits: ST 102 | } 103 | 104 | /* API: generate trait (regular variant) */ 105 | /* eslint no-redeclare: off */ 106 | export function trait< 107 | T extends ConsFactory 108 | > (factory: T): Trait 109 | 110 | /* API: generate trait (super-trait variant) */ 111 | export function trait< 112 | const ST extends (Trait | TypeFactory)[], 113 | T extends ConsFactory ? ExtractFactory> : 116 | First extends Trait ? ExtractFactory : 117 | any 118 | ) : any 119 | > 120 | > (superTraits: ST, factory: T): Trait 121 | 122 | /* API: generate trait (technical implementation) */ 123 | export function trait< 124 | const ST extends (Trait | TypeFactory)[], 125 | T extends ConsFactory ? ExtractFactory> : 128 | First extends Trait ? ExtractFactory : 129 | any 130 | ) : any 131 | > 132 | > (...args: any[]): Trait { 133 | const factory: T = (args.length === 2 ? args[1] : args[0]) 134 | const superTraits: ST = (args.length === 2 ? args[0] : undefined) 135 | return { 136 | id: crc32(factory.toString()), 137 | symbol: Symbol("trait"), 138 | factory, 139 | superTraits 140 | } 141 | } 142 | 143 | /* ==== TRAIT DERIVATION ==== */ 144 | 145 | /* ---- TRAIT PART EXTRACTION ---- */ 146 | 147 | /* utility types: extract factory from a trait */ 148 | type ExtractFactory< 149 | T extends Trait 150 | > = 151 | T extends Trait< 152 | ConsFactory, 153 | TraitDefTypeST 154 | > ? C : never 155 | 156 | /* utility types: extract supertraits from a trait */ 157 | type ExtractSuperTrait< 158 | T extends Trait 159 | > = 160 | T extends Trait< 161 | TraitDefTypeT, 162 | infer ST extends TraitDefTypeST 163 | > ? ST : never 164 | 165 | /* ---- TRAIT CONSTRUCTOR DERIVATION ---- */ 166 | 167 | /* utility type: derive type constructor: merge two constructors */ 168 | type DeriveTraitsConsConsMerge< 169 | A extends Cons, 170 | B extends Cons 171 | > = 172 | A extends (new (...args: infer ArgsA) => infer RetA) ? ( 173 | B extends (new (...args: infer ArgsB) => infer RetB) ? ( 174 | new (...args: MixParams) => RetA & RetB 175 | ) : never 176 | ) : never 177 | 178 | /* utility type: derive type constructor: extract plain constructor */ 179 | type DeriveTraitsConsCons< 180 | T extends Cons 181 | > = 182 | new (...args: ConstructorParameters) => InstanceType 183 | 184 | /* utility type: derive type constructor: from trait parts */ 185 | type DeriveTraitsConsTraitParts< 186 | C extends Cons, 187 | ST extends ((Trait | TypeFactory)[] | undefined) 188 | > = 189 | ST extends undefined ? DeriveTraitsConsCons : 190 | ST extends [] ? DeriveTraitsConsCons : 191 | DeriveTraitsConsConsMerge< 192 | DeriveTraitsConsCons, 193 | DeriveTraitsConsAll> /* RECURSION */ 194 | 195 | /* utility type: derive type constructor: from single trait */ 196 | type DeriveTraitsConsTrait< 197 | T extends Trait 198 | > = 199 | DeriveTraitsConsTraitParts< 200 | ExtractFactory, 201 | ExtractSuperTrait> 202 | 203 | /* utility type: derive type constructor: from single trait or trait factory */ 204 | type DeriveTraitsConsOne< 205 | T extends (Trait | TypeFactory) 206 | > = 207 | T extends Trait ? DeriveTraitsConsTrait : 208 | T extends TypeFactory ? DeriveTraitsConsTrait> : 209 | never 210 | 211 | /* utility type: derive type constructor: from one or more traits or trait factories */ 212 | type DeriveTraitsConsAll< 213 | T extends (((Trait | TypeFactory)[] | [ ...(Trait | TypeFactory)[], Cons ]) | undefined) 214 | > = 215 | T extends [ ...infer Others extends (Trait | TypeFactory)[], infer Last extends Cons ] ? ( 216 | DeriveTraitsConsConsMerge< 217 | DeriveTraitsConsAll, /* RECURSION */ 218 | DeriveTraitsConsCons> 219 | ) : 220 | T extends (Trait | TypeFactory)[] ? ( 221 | T extends [ infer First extends (Trait | TypeFactory) ] ? ( 222 | DeriveTraitsConsOne 223 | ) : ( 224 | T extends [ 225 | infer First extends (Trait | TypeFactory), 226 | ...infer Rest extends (Trait | TypeFactory)[] ] ? ( 227 | DeriveTraitsConsConsMerge< 228 | DeriveTraitsConsOne, 229 | DeriveTraitsConsAll> /* RECURSION */ 230 | ) : never 231 | ) 232 | ) : never 233 | 234 | /* utility type: derive type constructor */ 235 | type DeriveTraitsCons< 236 | T extends ((Trait | TypeFactory)[] | [ ...(Trait | TypeFactory)[], Cons ]) 237 | > = 238 | DeriveTraitsConsAll 239 | 240 | /* ---- TRAIT STATICS DERIVATION ---- */ 241 | 242 | /* utility type: derive type statics: merge two objects with statics */ 243 | type DeriveTraitsStatsConsMerge< 244 | T1 extends {}, 245 | T2 extends {} 246 | > = 247 | T1 & T2 248 | 249 | /* utility type: derive type statics: extract plain statics */ 250 | type DeriveTraitsStatsCons< 251 | T extends Cons 252 | > = 253 | Explode 254 | 255 | /* utility type: derive type statics: from trait parts */ 256 | type DeriveTraitsStatsTraitParts< 257 | C extends Cons, 258 | ST extends ((Trait | TypeFactory)[] | undefined) 259 | > = 260 | ST extends undefined ? DeriveTraitsStatsCons : 261 | ST extends [] ? DeriveTraitsStatsCons : 262 | DeriveTraitsStatsConsMerge< 263 | DeriveTraitsStatsCons, 264 | DeriveTraitsStatsAll> /* RECURSION */ 265 | 266 | /* utility type: derive type statics: from single trait */ 267 | type DeriveTraitsStatsTrait< 268 | T extends Trait 269 | > = 270 | DeriveTraitsStatsTraitParts< 271 | ExtractFactory, 272 | ExtractSuperTrait> 273 | 274 | /* utility type: derive type statics: from single trait or trait factory */ 275 | type DeriveTraitsStatsOne< 276 | T extends (Trait | TypeFactory) 277 | > = 278 | T extends Trait ? DeriveTraitsStatsTrait : 279 | T extends TypeFactory ? DeriveTraitsStatsTrait> : 280 | never 281 | 282 | /* utility type: derive type statics: from one or more traits or trait factories */ 283 | type DeriveTraitsStatsAll< 284 | T extends (((Trait | TypeFactory)[] | [ ...(Trait | TypeFactory)[], Cons ]) | undefined) 285 | > = 286 | T extends [ ...infer Others extends (Trait | TypeFactory)[], infer Last extends Cons ] ? ( 287 | DeriveTraitsStatsConsMerge< 288 | DeriveTraitsStatsAll, /* RECURSION */ 289 | DeriveTraitsStatsCons> 290 | ) : 291 | T extends (Trait | TypeFactory)[] ? ( 292 | T extends [ infer First extends (Trait | TypeFactory) ] ? ( 293 | DeriveTraitsStatsOne 294 | ) : ( 295 | T extends [ 296 | infer First extends (Trait | TypeFactory), 297 | ...infer Rest extends (Trait | TypeFactory)[] ] ? ( 298 | DeriveTraitsStatsConsMerge< 299 | DeriveTraitsStatsOne, 300 | DeriveTraitsStatsAll> /* RECURSION */ 301 | ) : never 302 | ) 303 | ) : never 304 | 305 | /* utility type: derive type statics */ 306 | type DeriveTraitsStats< 307 | T extends ((Trait | TypeFactory)[] | [ ...(Trait | TypeFactory)[], Cons ]) 308 | > = 309 | DeriveTraitsStatsAll 310 | 311 | /* ---- TRAIT DERIVATION ---- */ 312 | 313 | /* utility type: derive type from one or more traits or trait type factories */ 314 | type DeriveTraits< 315 | T extends ((Trait | TypeFactory)[] | [ ...(Trait | TypeFactory)[], Cons ]) 316 | > = 317 | DeriveTraitsCons & 318 | DeriveTraitsStats 319 | 320 | /* ---- TRAIT DERIVATION RUNTIME ---- */ 321 | 322 | /* utility function: add an additional invisible property to an object */ 323 | const extendProperties = 324 | (cons: Cons, field: string | symbol, value: any) => 325 | Object.defineProperty(cons, field, { value, enumerable: false, writable: false }) 326 | 327 | /* utility function: get raw trait */ 328 | const rawTrait = (x: (Trait | TypeFactory)) => 329 | isTypeFactory(x) ? x() : x 330 | 331 | /* utility function: derive a trait */ 332 | const deriveTrait = ( 333 | trait$: Trait | TypeFactory, 334 | baseClz: Cons, 335 | derived: Map 336 | ) => { 337 | /* get real trait */ 338 | const trait = rawTrait(trait$) 339 | 340 | /* start with base class */ 341 | let clz = baseClz 342 | 343 | /* in case we still have not derived this trait... */ 344 | if (!derived.has(trait.id)) { 345 | derived.set(trait.id, true) 346 | 347 | /* iterate over all of its super traits */ 348 | if (trait.superTraits !== undefined) 349 | for (const superTrait of reverseTraitList(trait.superTraits)) 350 | clz = deriveTrait(superTrait, clz, derived) /* RECURSION */ 351 | 352 | /* derive this trait */ 353 | clz = trait.factory(clz) 354 | extendProperties(clz, "id", crc32(trait.factory.toString())) 355 | extendProperties(clz, trait.symbol, true) 356 | } 357 | 358 | return clz 359 | } 360 | 361 | /* utility function: get reversed trait list */ 362 | const reverseTraitList = (traits: (Trait | TypeFactory)[]) => 363 | traits.slice().reverse() as (Trait | TypeFactory)[] 364 | 365 | /* API: type derive */ 366 | export function derive 367 | , ...(Trait | TypeFactory)[] ] | 369 | [ ...(Trait | TypeFactory)[], Cons ] 370 | )> 371 | (...traits: T): DeriveTraits { 372 | /* run-time sanity check */ 373 | if (traits.length === 0) 374 | throw new Error("invalid number of parameters (expected one or more traits)") 375 | 376 | /* determine the base class (clz) and the list of traits (lot) */ 377 | let clz: Cons 378 | let lot: (Trait | TypeFactory)[] 379 | const last = traits[traits.length - 1] 380 | if (isCons(last) && !isTypeFactory(last)) { 381 | /* case 1: with trailing regular class */ 382 | clz = last 383 | lot = traits.slice(0, traits.length - 1) as (Trait | TypeFactory)[] 384 | } 385 | else { 386 | /* case 2: just regular traits or trait type factories */ 387 | clz = class ROOT {} 388 | lot = traits as (Trait | TypeFactory)[] 389 | } 390 | 391 | /* track already derived traits */ 392 | const derived = new Map() 393 | 394 | /* iterate over all traits */ 395 | for (const trait of reverseTraitList(lot)) 396 | clz = deriveTrait(trait, clz, derived) 397 | 398 | return clz as DeriveTraits 399 | } 400 | 401 | /* ==== TRAIT TYPE-GUARDING ==== */ 402 | 403 | /* internal type: implements trait type */ 404 | type DerivedType = 405 | InstanceType> 406 | 407 | /* internal type: implements trait type or trait type factory */ 408 | type Derived | Cons)> = 409 | T extends TypeFactory ? DerivedType> : 410 | T extends Trait ? DerivedType : 411 | T extends Cons ? T : 412 | never 413 | 414 | /* API: type guard for checking whether class instance is derived from a trait */ 415 | export function derived 416 | | Cons)> 417 | (instance: unknown, trait: T): instance is Derived { 418 | /* ensure the class instance is really an object */ 419 | if (typeof instance !== "object") 420 | return false 421 | let obj = instance 422 | 423 | if (isCons(trait) && !isTypeFactory(trait)) { 424 | /* special case: regular class */ 425 | return (instance instanceof trait) 426 | } 427 | else { 428 | /* regular case: trait or trait type factory */ 429 | 430 | /* determine unique id of trait */ 431 | const t = (isTypeFactory(trait) ? trait() : trait) as Trait 432 | const idTrait = t["id"] 433 | 434 | /* iterate over class/trait hierarchy */ 435 | while (obj) { 436 | if (Object.hasOwn(obj, "constructor")) { 437 | const id = ((obj.constructor as any)["id"] as number) ?? 0 438 | if (id === idTrait) 439 | return true 440 | } 441 | obj = Object.getPrototypeOf(obj) 442 | } 443 | } 444 | return false 445 | } 446 | 447 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./etc/tsc.json" 3 | } 4 | --------------------------------------------------------------------------------