From 06f89d0a734c5436fda4b2aeece7b081fcdb3fb9 Mon Sep 17 00:00:00 2001 From: maddin1502 Date: Wed, 13 Nov 2024 23:07:21 +0100 Subject: [PATCH] fix typings (#21) - removed most any's - more comments/documentation - breaking change: removed generic from Enumerable type (it's not possible to differ between numeric and heterogeneous enum) - breaking change: optimized EventArgs (EventArgs, especially CancelEventArgs, can now be created without parameters) - updated dependencies - cleaned, optimized, simplified, fixed typings - new: scoping core --- .prettierrc | 1 + README.md | 95 +++++++++++++++++--- eslint.config.js | 2 +- package-lock.json | 144 +++++++++++++++--------------- package.json | 12 +-- src/array.ts | 29 ++++-- src/constructor.ts | 48 +++++++--- src/dictionary.ts | 41 ++++++++- src/disposable/base.ts | 2 + src/disposable/index.ts | 3 + src/emptyObject.ts | 11 ++- src/enumerable.ts | 80 +++++++++++++---- src/event/args/cancel.ts | 13 +-- src/event/args/index.ts | 23 ++++- src/event/handler.ts | 3 + src/event/index.ts | 3 + src/event/types.ts | 22 +++++ src/index.ts | 38 +++----- src/mapping.ts | 4 + src/scope.ts | 166 +++++++++++++++++++++++++++++++++++ src/shape.ts | 44 +++++++++- tests/code/event.test.ts | 22 ++--- tests/code/index.test.ts | 5 +- tests/code/scope.test.ts | 99 +++++++++++++++++++++ tests/code/types.test.ts | 23 ++--- tests/readme/scoping.test.ts | 80 +++++++++++++++++ 26 files changed, 818 insertions(+), 195 deletions(-) create mode 100644 src/scope.ts create mode 100644 tests/code/scope.test.ts create mode 100644 tests/readme/scoping.test.ts diff --git a/.prettierrc b/.prettierrc index 32ebab4..f83c991 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { + "$schema": "https://json.schemastore.org/prettierrc", "singleQuote": true, "trailingComma": "none" } diff --git a/README.md b/README.md index e60975f..3d38219 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,22 @@ [![npm downloads](https://badgen.net/npm/dw/ts-lib-extended)](https://badge.fury.io/js/ts-lib-extended) ## Features -- Enum type +- Enum type (key and value extraction - ignores reveres mapping) - Dictionary types (safe, readonly, key + value types) - Constructor types (abstract, standard, parameter + instance types) - Core class for disposable instances -- Events (handler, args) +- Events (handler, args, cancellation, external subscription + internal invocation) - Array types (minimal length array, item type) +- Enforce Empty Object type (only allows the assignment of empty objects) +- Scoped instances (create tree structures) ## Installation ```bash npm i ts-lib-extended ``` -or -```bash -yarn add ts-lib-extended -``` - ## Events -With events it is possible to subscribe to specific action or change on an instance. +With events it is possible to subscribe to a specific action or change on an instance. This is inspired by C# and should work in a similar way. ```ts @@ -215,12 +212,12 @@ enum NumberEnum { bruce } -function doSomethingWithEnum({}: Enumerable): void { +function doSomethingWithEnum(enum_: Enumerable): void { /** crazy code here */ } -doSomethingWithEnum(NumberEnum); // TS Error - function argument is limited to string enum type -doSomethingWithEnum(MyEnum); // NO error +doSomethingWithEnum(NumberEnum); +doSomethingWithEnum(MyEnum); ``` ## Gain keys and values @@ -272,3 +269,79 @@ class SpecialWithoutParams extends Special { } } ``` + +# Scoping + +Extend your classes from ScopedInstanceCore to get quick access to scopes. Scopes allow you to create a tree structure within your class instance. Variants can be used to create custom instances per scope. + +```ts +import { + InstanceScopeCore, + ScopedInstanceCore, + type InstanceScope +} from 'ts-lib-extended'; + +type MyScopeVariants = 'dark' | 'light'; + +class MyClass extends ScopedInstanceCore { + constructor(public readonly user?: string) { + super(); + } + + protected disposeScope(scope_: MyClassScope): void { + scope_.dispose(); + } + + protected createScope(scopeId_: PropertyKey): MyClassScope { + return new MyClassScope(scopeId_); + } +} + +class MyClassScope + extends InstanceScopeCore + implements InstanceScope +{ + public get dark(): MyClass { + return this.getOrCreateInstance('dark'); + } + + public get light(): MyClass { + return this.getOrCreateInstance('light'); + } + + protected createInstance(variant_: MyScopeVariants): MyClass { + let user: string; + + console.log(this.scopeId, variant_); + + if (variant_ === 'dark') { + if (this.scopeId === 'starwars') { + user = 'Anakin Skywalker'; + } else { + user = 'Riku'; + } + } else { + if (this.scopeId === 'starwars') { + user = 'Luke Skywalker'; + } else { + user = 'Sora'; + } + } + + return new MyClass(user); + } + + protected disposeInstance(instance_: MyClass): void { + instance_.dispose(); + } +} + +const mc = new MyClass(); +const starwarsScope = mc.scope('starwars'); +starwarsScope.dark.user; // => Anakin Skywalker +starwarsScope.light.user; // => Luke Skywalker + +const kingdomheartsScope = mc.scope('kingdomhearts'); // or any other scope id +kingdomheartsScope.dark.user; // => Riku +kingdomheartsScope.light.user; // => Sora +``` diff --git a/eslint.config.js b/eslint.config.js index e05f6a3..32f8a03 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -25,7 +25,7 @@ export default [ '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-var-requires': 'warn', - '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-explicit-any': ['warn', { ignoreRestArgs: true }], '@typescript-eslint/no-shadow': 'warn', '@typescript-eslint/no-unsafe-assignment': 'warn', '@typescript-eslint/no-unsafe-return': 'warn', diff --git a/package-lock.json b/package-lock.json index fd2afb0..451b3cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,23 @@ { "name": "ts-lib-extended", - "version": "3.0.4", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ts-lib-extended", - "version": "3.0.4", + "version": "4.0.0", "license": "MIT", "devDependencies": { - "@eslint/js": "^9.12.0", - "@types/node": "^20.12.4", + "@eslint/js": "^9.13.0", + "@types/node": "^20.16.13", "@vitest/coverage-v8": "^2.1.3", "@vitest/ui": "^2.1.3", - "eslint": "^9.12.0", + "eslint": "^9.13.0", "globals": "^15.11.0", "tsc-alias": "^1.8.10", - "typescript": "<5.6.0", - "typescript-eslint": "^8.9.0", + "typescript": "~5.5", + "typescript-eslint": "^8.10.0", "vitest": "^2.1.3" } }, @@ -539,9 +539,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", - "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -586,9 +586,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", - "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", "dev": true, "license": "MIT", "engines": { @@ -606,9 +606,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1046,9 +1046,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", - "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "version": "20.16.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz", + "integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==", "dev": true, "license": "MIT", "dependencies": { @@ -1056,17 +1056,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.9.0.tgz", - "integrity": "sha512-Y1n621OCy4m7/vTXNlCbMVp87zSd7NH0L9cXD8aIpOaNlzeWxIK4+Q19A68gSmTNRZn92UjocVUWDthGxtqHFg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz", + "integrity": "sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/type-utils": "8.9.0", - "@typescript-eslint/utils": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/type-utils": "8.10.0", + "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1090,16 +1090,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.9.0.tgz", - "integrity": "sha512-U+BLn2rqTTHnc4FL3FJjxaXptTxmf9sNftJK62XLz4+GxG3hLHm/SUNaaXP5Y4uTiuYoL5YLy4JBCJe3+t8awQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.10.0.tgz", + "integrity": "sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/typescript-estree": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4" }, "engines": { @@ -1119,14 +1119,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.9.0.tgz", - "integrity": "sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz", + "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0" + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1137,14 +1137,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.9.0.tgz", - "integrity": "sha512-JD+/pCqlKqAk5961vxCluK+clkppHY07IbV3vett97KOV+8C6l+CPEPwpUuiMwgbOz/qrN3Ke4zzjqbT+ls+1Q==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz", + "integrity": "sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.9.0", - "@typescript-eslint/utils": "8.9.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/utils": "8.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1162,9 +1162,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.9.0.tgz", - "integrity": "sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz", + "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==", "dev": true, "license": "MIT", "engines": { @@ -1176,14 +1176,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.9.0.tgz", - "integrity": "sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz", + "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1231,16 +1231,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.9.0.tgz", - "integrity": "sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz", + "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/typescript-estree": "8.9.0" + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1254,13 +1254,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.9.0.tgz", - "integrity": "sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz", + "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.9.0", + "@typescript-eslint/types": "8.10.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1889,18 +1889,18 @@ } }, "node_modules/eslint": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", - "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.6.0", + "@eslint/core": "^0.7.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.12.0", + "@eslint/js": "9.13.0", "@eslint/plugin-kit": "^0.2.0", "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", @@ -3567,15 +3567,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.9.0.tgz", - "integrity": "sha512-AuD/FXGYRQyqyOBCpNLldMlsCGvmDNxptQ3Dp58/NXeB+FqyvTfXmMyba3PYa0Vi9ybnj7G8S/yd/4Cw8y47eA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.10.0.tgz", + "integrity": "sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.9.0", - "@typescript-eslint/parser": "8.9.0", - "@typescript-eslint/utils": "8.9.0" + "@typescript-eslint/eslint-plugin": "8.10.0", + "@typescript-eslint/parser": "8.10.0", + "@typescript-eslint/utils": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index c325489..5ae5bd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-lib-extended", - "version": "3.0.4", + "version": "4.0.0", "description": "Additional types and tools for typescript", "files": [ "dist" @@ -46,15 +46,15 @@ "access": "public" }, "devDependencies": { - "@eslint/js": "^9.12.0", - "@types/node": "^20.12.4", + "@eslint/js": "^9.13.0", + "@types/node": "^20.16.13", "@vitest/coverage-v8": "^2.1.3", "@vitest/ui": "^2.1.3", - "eslint": "^9.12.0", + "eslint": "^9.13.0", "globals": "^15.11.0", "tsc-alias": "^1.8.10", - "typescript": "<5.6.0", - "typescript-eslint": "^8.9.0", + "typescript": "~5.5", + "typescript-eslint": "^8.10.0", "vitest": "^2.1.3" } } diff --git a/src/array.ts b/src/array.ts index 8e79dd1..90db930 100644 --- a/src/array.ts +++ b/src/array.ts @@ -1,9 +1,17 @@ -export type ArrayItem | ArrayLike> = - T extends { [Symbol.iterator](): IterableIterator } - ? P - : T extends ArrayLike - ? P - : never; +/** + * determine type of array items + * + * @export + * @template {IterableIterator | ArrayLike} T + * @since 1.1.0 + */ +export type ArrayItem< + T extends IterableIterator | ArrayLike +> = T extends { [Symbol.iterator](): IterableIterator } + ? P + : T extends ArrayLike + ? P + : never; type BuildMinArray< TItem, @@ -12,6 +20,15 @@ type BuildMinArray< > = TFix['length'] extends TMin ? TFix : BuildMinArray; + +/** + * Array with a minimum number of items + * + * @export + * @template TItem + * @template {number} TMin + * @since 1.1.0 + */ export type MinArray = [ ...BuildMinArray, ...TItem[] diff --git a/src/constructor.ts b/src/constructor.ts index 12d6a4a..5385828 100644 --- a/src/constructor.ts +++ b/src/constructor.ts @@ -1,22 +1,44 @@ -export type StandardConstructor = new ( - ...args: A +/** + * non-abstract class constructor + * + * @export + * @template [T=unknown] + * @since 1.0.0 + */ +export type StandardConstructor = new (...args: any[]) => T; + +/** + * abstract class constructor + * + * @export + * @template [T=unkown] + * @since 1.0.0 + */ +export type AbstractConstructor = abstract new ( + ...params: any[] ) => T; -export type AbstractConstructor< - T = any, - A extends any[] = any[] -> = abstract new (...params: A) => T; -export type Constructor = + +/** + * abstract or non-abstract class constructor + * + * @export + * @template [T=any] + * @since 1.0.0 + */ +export type Constructor = | StandardConstructor | AbstractConstructor; + +/** + * Type of an instance created with a constructor + * + * @export + * @template {Constructor} C + * @since 1.0.0 + */ export type ConstructorInstance = C extends StandardConstructor ? Instance : C extends AbstractConstructor ? Instance : never; -export type ConstructorParameters = - C extends StandardConstructor - ? Arguments - : C extends AbstractConstructor - ? Arguments - : never; diff --git a/src/dictionary.ts b/src/dictionary.ts index 919b4a6..c40a9ec 100644 --- a/src/dictionary.ts +++ b/src/dictionary.ts @@ -1,9 +1,44 @@ -export type Dictionary = { +/** + * safe dictionary (values are always optional) + * + * @export + * @template [T=unknown] + * @template {string | number} [K=string] + * @since 1.0.0 + */ +export type Dictionary = { [key in K]?: T; }; + +/** + * readonly safe dictionary + * + * @export + * @template [T=unknown] + * @template {string | number} [K=string] + * @since 1.0.0 + */ export type ReadonlyDictionary< - T = any, + T = unknown, K extends string | number = string > = Readonly>; -export type DictionaryKey = D extends Dictionary ? P : never; + +/** + * type of dictionary keys + * + * @export + * @template D + * @since 1.0.0 + */ +export type DictionaryKey = D extends Dictionary + ? P + : never; + +/** + * type of dictionary values + * + * @export + * @template D + * @since 1.0.0 + */ export type DictionaryValue = D extends Dictionary ? P : never; diff --git a/src/disposable/base.ts b/src/disposable/base.ts index 9ccd800..e98dad3 100644 --- a/src/disposable/base.ts +++ b/src/disposable/base.ts @@ -4,6 +4,7 @@ * @export * @abstract * @class DisposableBase + * @since 1.0.0 */ export abstract class DisposableBase { protected _isDisposed: boolean; @@ -21,6 +22,7 @@ export abstract class DisposableBase { * * @return {*} {void} * @memberof DisposableBase + * @since 1.0.0 */ public dispose(): void { if (this.isDisposed) { diff --git a/src/disposable/index.ts b/src/disposable/index.ts index 92e9108..a9afba8 100644 --- a/src/disposable/index.ts +++ b/src/disposable/index.ts @@ -8,6 +8,7 @@ import { DisposableBase } from './base.js'; * @export * @class Disposable * @extends {DisposableBase} + * @since 1.0.0 */ export class Disposable extends DisposableBase { private _disposingHandler: EventHandler; @@ -27,6 +28,7 @@ export class Disposable extends DisposableBase { * @readonly * @type {Event} * @memberof Disposable + * @since 1.0.0 */ public get disposing(): Event { return this._disposingHandler.event; @@ -38,6 +40,7 @@ export class Disposable extends DisposableBase { * @readonly * @type {Event} * @memberof Disposable + * @since 1.0.0 */ public get disposed(): Event { return this._disposedHandler.event; diff --git a/src/emptyObject.ts b/src/emptyObject.ts index 55f01e1..1a8f88f 100644 --- a/src/emptyObject.ts +++ b/src/emptyObject.ts @@ -1,2 +1,9 @@ -const empty = Symbol(); -export type EmptyObject = { [empty]?: never }; +/** + * enforces empty object (= object with no props). + * + * Type "{}" is not the same, instead of representing an empty object, it represents any value except null and undefined. + * + * @export + * @since 1.0.0 + */ +export type EmptyObject = Record; diff --git a/src/enumerable.ts b/src/enumerable.ts index b4dae6d..ecabeb7 100644 --- a/src/enumerable.ts +++ b/src/enumerable.ts @@ -1,16 +1,54 @@ -export type Enumerable = { - [id: string]: T | string; +import type { Prettify } from './mapping.js'; + +/** + * enum type representation + * + * ```ts + * enum MyEnum { a, b } + * + * function doMagic(enum: Enumerable): void { ... } + * + * doMagic(MyEnum) + * ``` + * + * @export + * @since 1.0.0 + */ +export type Enumerable = { + [id: string]: number | string; [nu: number]: string; }; -export type EnumerableValue = - TEnum extends Record - ? K extends string - ? V - : never - : never; +/** + * enum values type + * + * ``` + * enum MyEnum { a, b } + * EnumerableValue => MyEnum.a | MyEnum.b + * ``` + * + * @export + * @template {Enumerable} TEnum + * @since 1.1.5 + */ +export type EnumerableValue = Prettify< + TEnum[keyof TEnum] +>; -export type EnumerableBase = +/** + * enum values base type + * + * ``` + * String Enum => string + * Numeric Enum => number + * Heterogeneous Enum => string | number + * ``` + * + * @export + * @template {EnumerableValue} TEnumValue + * @since 1.1.5 + */ +export type EnumerableBase> = TEnumValue extends string ? TEnumValue extends number ? string | number @@ -19,10 +57,18 @@ export type EnumerableBase = ? number : never; -export type EnumerableEntry = [ - keyof E, - EnumerableValue -]; +/** + * tuple of enum key and associated value + * + * @export + * @template {Enumerable} E + * @since 1.2.0 + */ +export type EnumerableEntry = { + [key in keyof E]: [key, E[key]]; +} extends Record + ? Exclude + : never; /** * Gain keys and values from enum instances. Works with string, numeric and mixed enums @@ -30,6 +76,7 @@ export type EnumerableEntry = [ * * @export * @class EnumerableObject + * @since 1.2.0 */ export class EnumerableObject { /** @@ -39,15 +86,14 @@ export class EnumerableObject { * @template {Enumerable} E * @param {E} enum_ * @returns {ReadonlyArray>} + * @since 1.2.0 */ public values( enum_: E ): ReadonlyArray> { const values: EnumerableValue[] = []; - this.disassemble(enum_, (key_) => - values.push(enum_[key_] as EnumerableValue) - ); + this.disassemble(enum_, (key_) => values.push(enum_[key_])); return values; } @@ -59,6 +105,7 @@ export class EnumerableObject { * @template {Enumerable} E * @param {E} enum_ * @returns {ReadonlyArray} + * @since 1.2.0 */ public keys(enum_: E): ReadonlyArray { const keys: (keyof E)[] = []; @@ -75,6 +122,7 @@ export class EnumerableObject { * @template {Enumerable} E * @param {E} enum_ * @returns {ReadonlyArray>} + * @since 1.2.0 */ public entries( enum_: E diff --git a/src/event/args/cancel.ts b/src/event/args/cancel.ts index 74ee18a..2244985 100644 --- a/src/event/args/cancel.ts +++ b/src/event/args/cancel.ts @@ -5,12 +5,15 @@ import { EventArgs } from './index.js'; * * @export * @class CancelEventArgs - * @template [TValue=any] - * @extends {EventArgs} + * @template {unknown[]} [TArgs=unknown[]] + * @extends {EventArgs} + * @since 1.0.0 */ -export class CancelEventArgs extends EventArgs { - constructor(value_: TValue) { - super(value_); +export class CancelEventArgs< + TArgs extends unknown[] = unknown[] +> extends EventArgs { + constructor(...args_: TArgs) { + super(...args_); this.cancel = false; } diff --git a/src/event/args/index.ts b/src/event/args/index.ts index 0890d4c..0a067e7 100644 --- a/src/event/args/index.ts +++ b/src/event/args/index.ts @@ -3,8 +3,25 @@ * * @export * @class EventArgs - * @template [TValue=any] + * @template {unknown[]} [TArgs=unknown[]] + * @since 1.0.0 */ -export class EventArgs { - constructor(public readonly value: TValue) {} +export class EventArgs { + private _args: TArgs; + + constructor(...args_: TArgs) { + this._args = args_; + } + + /** + * passed arguments + * + * @public + * @readonly + * @type {TArgs} + * @since 4.0.0 + */ + public get args(): TArgs { + return this._args; + } } diff --git a/src/event/handler.ts b/src/event/handler.ts index 3ce50ef..a3256fa 100644 --- a/src/event/handler.ts +++ b/src/event/handler.ts @@ -13,6 +13,7 @@ import type { EventCallback } from './types.js'; * @template TSender * @template {EventArgs | void} [TArgs=void] * @extends {DisposableBase} + * @since 1.0.0 */ export class EventHandler< TSender, @@ -40,6 +41,7 @@ export class EventHandler< * @readonly * @type {Event} * @memberof EventHandler + * @since 1.0.0 */ public get event(): Event { return this.validateDisposed(this._event); @@ -51,6 +53,7 @@ export class EventHandler< * @param {TSender} sender_ * @param {TArgs} eventArgs_ * @memberof EventHandler + * @since 1.0.0 */ public invoke(sender_: TSender, eventArgs_: TArgs): void { const keys = Object.keys(this._callbacks); diff --git a/src/event/index.ts b/src/event/index.ts index dc83a91..0a106bb 100644 --- a/src/event/index.ts +++ b/src/event/index.ts @@ -8,6 +8,7 @@ import type { EventSubscription, EventUnsubscription } from './types.js'; * @class Event * @template TSender * @template {EventArgs | void} [TArgs=void] + * @since 1.0.0 */ export class Event { private _subscription: @@ -35,6 +36,7 @@ export class Event { * @readonly * @type {EventSubscription} * @memberof Event + * @since 1.0.0 */ public get subscribe(): EventSubscription { return this.validateDetached(this._subscription?.subscribe); @@ -46,6 +48,7 @@ export class Event { * @readonly * @type {EventUnsubscription} * @memberof Event + * @since 1.0.0 */ public get unsubscribe(): EventUnsubscription { return this.validateDetached(this._subscription?.unsubscribe); diff --git a/src/event/types.ts b/src/event/types.ts index e501e89..faff440 100644 --- a/src/event/types.ts +++ b/src/event/types.ts @@ -1,12 +1,34 @@ import type { EventArgs } from './args/index.js'; +/** + * Event subscriber + * + * @export + * @template TSender + * @template {EventArgs | void} [TArgs=void] + * @since 1.0.0 + */ export type EventSubscription< TSender, TArgs extends EventArgs | void = void > = (identifier_: string, callback_: EventCallback) => boolean; +/** + * Event unsubscriber + * + * @export + * @since 1.0.0 + */ export type EventUnsubscription = (identifier_: string) => boolean; +/** + * Event subscription callback that is triggered when event is invoked + * + * @export + * @template TSender + * @template {EventArgs | void} [TArgs=void] + * @since 1.0.0 + */ export type EventCallback = ( sender_: TSender, eventArgs_: TArgs diff --git a/src/index.ts b/src/index.ts index 504ba7a..663f9af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,39 +1,21 @@ import { EnumerableObject } from './enumerable.js'; -export type * from '@/shape.js'; -export type { ArrayItem, MinArray } from './array.js'; -export type { - AbstractConstructor, - Constructor, - ConstructorInstance, - ConstructorParameters, - StandardConstructor -} from './constructor.js'; -export type { - Dictionary, - DictionaryKey, - DictionaryValue, - ReadonlyDictionary -} from './dictionary.js'; +export type * from './array.js'; +export type * from './constructor.js'; +export type * from './dictionary.js'; export { DisposableBase } from './disposable/base.js'; export { Disposable } from './disposable/index.js'; -export type { EmptyObject } from './emptyObject.js'; -export type { - Enumerable, - EnumerableBase, - EnumerableEntry, - EnumerableValue -} from './enumerable.js'; +export type * from './emptyObject.js'; +export type * from './enumerable.js'; export { CancelEventArgs } from './event/args/cancel.js'; export { EventArgs } from './event/args/index.js'; export { EventHandler } from './event/handler.js'; export { Event } from './event/index.js'; -export type { - EventCallback, - EventSubscription, - EventUnsubscription -} from './event/types.js'; -export type { PublicMembers } from './mapping.js'; +export type * from './event/types.js'; +export type * from './mapping.js'; +export type * from './scope.js'; +export { InstanceScopeCore, ScopedInstanceCore } from './scope.js'; +export type * from './shape.js'; export { EnumerableObject }; export const enumerableObject = new EnumerableObject(); diff --git a/src/mapping.ts b/src/mapping.ts index 5f5da5e..d660216 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -1,3 +1,7 @@ export type PublicMembers = { [key in keyof T]: T[key]; }; + +export type Prettify = T & unknown; +// type Prettify2 = T extends T ? T : T; +// type Prettify3 = Pick; diff --git a/src/scope.ts b/src/scope.ts new file mode 100644 index 0000000..88cd5aa --- /dev/null +++ b/src/scope.ts @@ -0,0 +1,166 @@ +import { Disposable } from './disposable/index.js'; + +/** + * @export + * @template T + * @since 4.0.0 + */ +export type InstanceScopeVariants = { + /** + * list of all variants in scope + * + * @type {T[]} + * @since 4.0.0 + */ + variants: T[]; +}; + +/** + * instance scope that can be split into different variants + * + * @export + * @template T + * @template {string} Variant instance variants (e.g. 'dark' | 'light') + * @since 4.0.0 + */ +export type InstanceScope = { + readonly [key in Variant]: T; +} & InstanceScopeVariants; + +/** + * scopes cache + * + * @export + * @template {InstanceScope} S + * @since 4.0.0 + */ +export type ScopesSource = Map; + +/** + * @export + * @template {InstanceScope} S + * @since 4.0.0 + */ +export type ScopedInstance = { + /** + * get an instance scope by id + * + * @param {PropertyKey} scopeId_ + * @returns {S} + * @since 4.0.0 + */ + scope(scopeId_: PropertyKey): S; + /** + * list of all scopes + * + * @type {S[]} + * @since 4.0.0 + */ + scopes: S[]; +}; + +/** + * extend from this core class to get quick scope support + * + * @export + * @abstract + * @class ScopedInstanceCore + * @template {InstanceScope} S + * @extends {Disposable} + * @implements {ScopedInstance} + * @since 4.0.0 + */ +export abstract class ScopedInstanceCore + extends Disposable + implements ScopedInstance +{ + private readonly _source: ScopesSource; + + constructor() { + super(); + this._source = new Map(); + + this._disposers.push(() => { + this._source.forEach((scope_) => this.disposeScope(scope_)); + this._source.clear(); + }); + } + + protected abstract disposeScope(scope_: S): void; + + protected abstract createScope(id_: PropertyKey): S; + + public scope(id_: PropertyKey): S { + this.validateDisposed(this); + let scope = this._source.get(id_); + + if (!scope) { + scope = this.createScope(id_); + this._source.set(id_, scope); + } + + return scope; + } + + public get scopes(): S[] { + this.validateDisposed(this); + return [...this._source.values()]; + } +} + +/** + * scope variants cache + * + * @export + * @template T + * @template {string} Variant instance variants (e.g. 'dark' | 'light') + * @since 4.0.0 + */ +export type ScopeVariantsSource = Map; + +/** + * use this as scope base to access core features + * + * @export + * @abstract + * @class InstanceScopeCore + * @template T + * @template {string} Variant instance variants (e.g. 'dark' | 'light') + * @extends {Disposable} + * @implements {InstanceScopeVariants} + * @since 4.0.0 + */ +export abstract class InstanceScopeCore + extends Disposable + implements InstanceScopeVariants +{ + private _source: ScopeVariantsSource; + + constructor(public readonly scopeId: PropertyKey) { + super(); + this._source = new Map(); + + this._disposers.push(() => { + this._source.forEach((variante_) => this.disposeInstance(variante_)); + this._source.clear(); + }); + } + + public get variants(): T[] { + return [...this._source.values()]; + } + + protected getOrCreateInstance(variant_: Variant): T { + let instance = this._source.get(variant_); + + if (!instance) { + instance = this.createInstance(variant_); + this._source.set(variant_, instance); + } + + return instance; + } + + protected abstract createInstance(variant_: Variant): T; + protected abstract disposeInstance(instance_: T): void; +} diff --git a/src/shape.ts b/src/shape.ts index 0d3e47f..46c73e9 100644 --- a/src/shape.ts +++ b/src/shape.ts @@ -1,11 +1,47 @@ -export type MethodLike = (...args_: any[]) => any; -export type ObjectLike = Record; +/** + * cover functions, methods and arrow functions + * + * @export + * @since 3.0.2 + */ +export type MethodLike = (...args_: any[]) => unknown; + +type NotAFunction = + | { + caller?: void; + } + | { + bind?: void; + } + | { + apply?: void; + } + | { + call?: void; + }; + +/** + * cover all class instances, records, anonymus objects and arrays + * + * @alias ObjectLike (on legacy versions) + * @export + * @since 4.0.0 + */ +export type InstanceLike = Record & NotAFunction; + +/** + * cover all types like "any" or "unknown" + * + * @export + * @since 3.0.2 + */ export type AnyLike = | number | string | boolean - | ObjectLike + | InstanceLike | MethodLike | undefined | symbol - | null; + | null + | ArrayLike; diff --git a/tests/code/event.test.ts b/tests/code/event.test.ts index f81e0c4..6a74938 100644 --- a/tests/code/event.test.ts +++ b/tests/code/event.test.ts @@ -8,9 +8,9 @@ import { describe, expect, test } from 'vitest'; class TestSubject extends Disposable { private _valueChangingHandler: EventHandler< this, - CancelEventArgs<{ old: number; new: number }> + CancelEventArgs<[number, number]> >; - private _valueChangedHandler: EventHandler>; + private _valueChangedHandler: EventHandler>; private _value: number; constructor(initValue_: number) { @@ -24,14 +24,11 @@ class TestSubject extends Disposable { }); } - public get changing(): Event< - this, - CancelEventArgs<{ old: number; new: number }> - > { + public get changing(): Event> { return this._valueChangingHandler.event; } - public get changed(): Event> { + public get changed(): Event> { return this._valueChangedHandler.event; } @@ -39,10 +36,7 @@ class TestSubject extends Disposable { return this._value; } public set value(new_: number) { - const cancelArgs = new CancelEventArgs<{ old: number; new: number }>({ - old: this._value, - new: new_ - }); + const cancelArgs = new CancelEventArgs(this._value, new_); this._valueChangingHandler.invoke(this, cancelArgs); if (cancelArgs.cancel) { @@ -50,7 +44,7 @@ class TestSubject extends Disposable { } this._value = new_; - this._valueChangedHandler.invoke(this, new EventArgs(new_)); + this._valueChangedHandler.invoke(this, new EventArgs(new_)); } } @@ -68,14 +62,14 @@ describe(TestSubject.name, () => { subject.changing.subscribe('changing', (sender_, args_) => { expect(subject === sender_).toBe(true); changingCount++; - args_.cancel = args_.value.new === 21; + args_.cancel = args_.args[1] === 21; }) ).toBe(true); expect( subject.changed.subscribe('changed', (sender_, args_) => { expect(subject === sender_).toBe(true); changedCount++; - expect(args_.value).toBe(123456789); + expect(args_.args[0]).toBe(123456789); }) ).toBe(true); diff --git a/tests/code/index.test.ts b/tests/code/index.test.ts index 1000cc6..617d57f 100644 --- a/tests/code/index.test.ts +++ b/tests/code/index.test.ts @@ -3,20 +3,23 @@ import { Disposable, DisposableBase, EnumerableObject, + enumerableObject, Event, EventArgs, - enumerableObject + ScopedInstanceCore } from '@/index.js'; import { describe, expect, test } from 'vitest'; describe('main', () => { test('exports', () => { + expect.assertions(8); expect(Disposable).toBeDefined(); expect(DisposableBase).toBeDefined(); expect(Event).toBeDefined(); expect(EventArgs).toBeDefined(); expect(CancelEventArgs).toBeDefined(); expect(EnumerableObject).toBeDefined(); + expect(ScopedInstanceCore).toBeDefined(); expect(enumerableObject).toBeDefined(); }); }); diff --git a/tests/code/scope.test.ts b/tests/code/scope.test.ts new file mode 100644 index 0000000..0d9df6b --- /dev/null +++ b/tests/code/scope.test.ts @@ -0,0 +1,99 @@ +import { + InstanceScopeCore, + ScopedInstanceCore, + type InstanceScope +} from '@/scope.js'; +import { describe, expect, test } from 'vitest'; + +type TestVariant = 'a' | 'b'; + +class TestScope + extends InstanceScopeCore + implements InstanceScope +{ + public get a(): Test { + return this.getOrCreateInstance('a'); + } + + public get b(): Test { + return this.getOrCreateInstance('b'); + } + + protected createInstance(variant_: TestVariant): Test { + return new Test(this.scopeId, variant_); + } + + protected disposeInstance(instance_: Test): void { + instance_.dispose(); + } +} + +class Test extends ScopedInstanceCore { + constructor( + public readonly id: PropertyKey, + public readonly variant: TestVariant | 'root' + ) { + super(); + } + + protected disposeScope(scope_: TestScope): void { + scope_.dispose(); + } + protected createScope(id_: PropertyKey): TestScope { + return new TestScope(id_); + } +} + +describe(ScopedInstanceCore, () => { + test('general', () => { + expect.assertions(15); + const test = new Test('root', 'root'); + expect(test.scopes.length).toBe(0); + const scope1 = test.scope(1); + expect(test.scopes.length).toBe(1); + expect(scope1.variants.length).toBe(0); + + const scope1variantA = scope1.a; + expect(scope1.variants.length).toBe(1); + expect(scope1variantA.id).toBe(1); + expect(scope1variantA.variant).toBe('a'); + + const scope1variantB = scope1.b; + expect(scope1.variants.length).toBe(2); + expect(scope1variantB.id).toBe(1); + expect(scope1variantB.variant).toBe('b'); + + expect(test.isDisposed).toBe(false); + expect(scope1variantA.isDisposed).toBe(false); + expect(scope1variantB.isDisposed).toBe(false); + test.dispose(); + expect(test.isDisposed).toBe(true); + expect(scope1variantA.isDisposed).toBe(true); + expect(scope1variantB.isDisposed).toBe(true); + }); +}); + +describe(InstanceScopeCore, () => { + test('general', () => { + expect.assertions(13); + const scope = new TestScope('root'); + + expect(scope.variants.length).toBe(0); + const variantA = scope.a; + expect(variantA.id).toBe('root'); + expect(variantA.variant).toBe('a'); + expect(scope.variants.length).toBe(1); + const variantB = scope.b; + expect(variantB.id).toBe('root'); + expect(variantB.variant).toBe('b'); + expect(scope.variants.length).toBe(2); + + expect(scope.isDisposed).toBe(false); + expect(variantA.isDisposed).toBe(false); + expect(variantB.isDisposed).toBe(false); + scope.dispose(); + expect(scope.isDisposed).toBe(true); + expect(variantA.isDisposed).toBe(true); + expect(variantB.isDisposed).toBe(true); + }); +}); diff --git a/tests/code/types.test.ts b/tests/code/types.test.ts index 0c60bcf..8c39bb2 100644 --- a/tests/code/types.test.ts +++ b/tests/code/types.test.ts @@ -1,14 +1,17 @@ -import '@/array.js'; -import '@/constructor.js'; -import '@/dictionary.js'; -import '@/emptyObject.js'; -import '@/event/types.js'; -import '@/shape.js'; -import '@/mapping.js'; import { describe, expect, test } from 'vitest'; -describe('dummy types test', () => { - test('dummy', () => { - expect(true).toBe(true); +describe('import test', () => { + test('types', () => { + // test whether imports work + expect.assertions(1); + expect(Promise.all([ + import('@/array.js'), + import('@/constructor.js'), + import('@/dictionary.js'), + import('@/emptyObject.js'), + import('@/event/types.js'), + import('@/shape.js'), + import('@/mapping.js') + ])).resolves.not.toThrow(); }); }); diff --git a/tests/readme/scoping.test.ts b/tests/readme/scoping.test.ts new file mode 100644 index 0000000..5d3c4d6 --- /dev/null +++ b/tests/readme/scoping.test.ts @@ -0,0 +1,80 @@ +import { + InstanceScopeCore, + ScopedInstanceCore, + type InstanceScope +} from '@/index.js'; +import { describe, expect, test } from 'vitest'; + +type MyScopeVariants = 'dark' | 'light'; + +class MyClass extends ScopedInstanceCore { + constructor(public readonly user?: string) { + super(); + } + + protected disposeScope(scope_: MyClassScope): void { + scope_.dispose(); + } + + protected createScope(scopeId_: PropertyKey): MyClassScope { + return new MyClassScope(scopeId_); + } +} + +class MyClassScope + extends InstanceScopeCore + implements InstanceScope +{ + public get dark(): MyClass { + return this.getOrCreateInstance('dark'); + } + + public get light(): MyClass { + return this.getOrCreateInstance('light'); + } + + protected createInstance(variant_: MyScopeVariants): MyClass { + let user: string; + + console.log(this.scopeId, variant_); + + if (variant_ === 'dark') { + if (this.scopeId === 'starwars') { + user = 'Anakin Skywalker'; + } else { + user = 'Riku'; + } + } else { + if (this.scopeId === 'starwars') { + user = 'Luke Skywalker'; + } else { + user = 'Sora'; + } + } + + return new MyClass(user); + } + + protected disposeInstance(instance_: MyClass): void { + instance_.dispose(); + } +} + +const mc = new MyClass(); +const starwarsScope = mc.scope('starwars'); +starwarsScope.dark.user; // => Anakin Skywalker +starwarsScope.light.user; // => Luke Skywalker + +const kingdomheartsScope = mc.scope('kingdomhearts'); // or any other scope id +kingdomheartsScope.dark.user; // => Riku +kingdomheartsScope.light.user; // => Sora + +describe(MyClass, () => { + test('scoping', () => { + expect.assertions(4); + expect(starwarsScope.dark.user).toBe('Anakin Skywalker'); + expect(starwarsScope.light.user).toBe('Luke Skywalker'); + expect(kingdomheartsScope.dark.user).toBe('Riku'); + expect(kingdomheartsScope.light.user).toBe('Sora'); + }); +});