├── .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 | [](https://github.com/rse)
18 | [](https://github.com/rse)
19 |
20 | [](https://npmjs.com/@traits-ts/core)
21 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------