Skip to content

Commit b23c951

Browse files
authored
Merge pull request #11 from ivklgn/feat/branded-types
feat: branded types for context, subcontext and features
2 parents 5ba9a6a + 94f32cb commit b23c951

6 files changed

Lines changed: 111 additions & 27 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,34 @@ const error = cardPaymentError("BackendLogicError", "Payment failed", { extended
250250
error.emit({ extendedParams: { logLevel: "fatal" } });
251251
```
252252

253+
### Helper Functions and Types
254+
255+
#### AnyFeatureOfSubcontext
256+
257+
Allows you to explicitly specify the type for any feature of the given subcontext.
258+
259+
```ts
260+
import { createError } from "conway-errors";
261+
262+
const createErrorContext = createError([
263+
{ errorType: "FrontendLogicError" },
264+
{ errorType: "BackendLogicError" },
265+
] as const);
266+
267+
const context = createErrorContext("Context");
268+
const subcontext = context.subcontext("Subcontext");
269+
270+
const featureError1 = context.feature("Feature");
271+
const featureError2 = subcontext.feature("Feature");
272+
273+
function customErrorThrower(featureError: AnyFeatureOfSubcontext<typeof subcontext>) {
274+
// ...
275+
}
276+
277+
customErrorThrower(featureError1); // error
278+
customErrorThrower(featureError2); // ok
279+
```
280+
253281
## Acknowledgment for Contributions
254282

255283
<table>

README_RU.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,34 @@ const error = cardPaymentError("BackendLogicError", "Payment failed", { extended
251251
error.emit({ extendedParams: { logLevel: "fatal" } })
252252
```
253253

254+
### Вспомогательные функции и типы
255+
256+
#### AnyFeatureOfSubcontext
257+
258+
Позволяет явно указать тип для любой feature указанного подконтекста.
259+
260+
```ts
261+
import { createError } from "conway-errors";
262+
263+
const createErrorContext = createError([
264+
{ errorType: "FrontendLogicError" },
265+
{ errorType: "BackendLogicError" },
266+
] as const);
267+
268+
const context = createErrorContext("Context");
269+
const subcontext = context.subcontext("Subcontext");
270+
271+
const featureError1 = context.feature("Feature");
272+
const featureError2 = subcontext.feature("Feature");
273+
274+
function customErrorThrower(featureError: AnyFeatureOfSubcontext<typeof subcontext>) {
275+
// ...
276+
}
277+
278+
customErrorThrower(featureError1); // error
279+
customErrorThrower(featureError2); // ok
280+
```
281+
254282
## Благодарность за вклад
255283

256284
<table>

index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { test } from "uvu";
22
import { snoop } from "snoop";
33
import * as assert from "uvu/assert";
44

5-
import { createError, isConwayError, type IConwayError } from "./index";
5+
import { createError, isConwayError } from "./index";
66

77
test("without error types will throw always UnknownError", () => {
88
const createErrorContext = createError();

index.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,6 @@ type ErrorMap = Record<
100100
}
101101
>;
102102

103-
type FeatureFn<ErrorType extends string> = (
104-
featureName: string,
105-
featureContextExtendedParams?: ExtendedParams
106-
) => CreateErrorFn<ErrorType>;
107-
108103
type ErrorFnOptions = {
109104
originalError?: OriginalError;
110105
extendedParams?: ExtendedParams;
@@ -116,23 +111,37 @@ type CreateErrorFn<ErrorType extends string> = (
116111
options?: ErrorFnOptions
117112
) => IConwayError;
118113

119-
type ErrorSubcontext<ErrorType extends string> = {
114+
type Brand<T, B> = T & { __brand: B };
115+
116+
type ErrorSubcontext<Name extends string, ErrorType extends string> = Brand<Subcontext<Name, ErrorType>, Name>;
117+
type ErrorFeature<Name extends string, ErrorType extends string> = Brand<CreateErrorFn<ErrorType>, Name>;
118+
export type AnyFeatureOfSubcontext<S> = S extends ErrorSubcontext<infer Name, infer ErrorType>
119+
? ErrorFeature<`${Name}/${string}`, ErrorType>
120+
: never;
121+
122+
type Subcontext<Name extends string, ErrorType extends string> = {
120123
/**
121124
* Create a child context within the current context.
122125
*
123126
* @param {string} childContextName - The name of the child context.
124127
* @param {ExtendedParams} extendedParams - Additional extended parameters for the child context.
125128
* @return {Function} Function to create an error context with the specified child context name and extended params.
126129
*/
127-
subcontext: (subcontextName: string, extendedParams?: ExtendedParams) => ErrorSubcontext<ErrorType>;
130+
subcontext: <const ChildContextName extends string>(
131+
subcontextName: ChildContextName,
132+
extendedParams?: ExtendedParams
133+
) => ErrorSubcontext<`${Name}/${ChildContextName}`, ErrorType>;
128134
/**
129135
* Creates a child feature within the current context.
130136
*
131137
* @param {string} childFeatureName - The name of the child feature.
132138
* @param {ExtendedParams} [extendedParams={}] - Additional extended parameters for the child feature.
133139
* @return {Function} The created error feature.
134140
*/
135-
feature: FeatureFn<ErrorType>;
141+
feature: <const FeatureName extends string>(
142+
featureName: FeatureName,
143+
featureContextExtendedParams?: ExtendedParams
144+
) => ErrorFeature<`${Name}/${FeatureName}`, ErrorType>;
136145
};
137146

138147
/**
@@ -146,7 +155,7 @@ export function createError<ErrorTypes extends ErrorTypeConfig>(errorTypes?: Err
146155
const _options = { ...defaultErrorOptions, ...options };
147156
const initialExtendedParams = options?.extendedParams ?? {};
148157

149-
return (contextName: string, extendedParams: ExtendedParams = {}) => {
158+
return <const ContextName extends string>(contextName: ContextName, extendedParams: ExtendedParams = {}) => {
150159
const outerExtendedParams = { ...initialExtendedParams, ...extendedParams };
151160

152161
const errorsMap: ErrorMap = Array.isArray(errorTypes)
@@ -162,30 +171,37 @@ export function createError<ErrorTypes extends ErrorTypeConfig>(errorTypes?: Err
162171
const UnknownError = createErrorClass("UnknownError", contextName);
163172

164173
const _createSubcontext =
165-
(contextName: string, subContextExtendedParams: ExtendedParams) =>
166-
(childContextName: string, extendedParams: ExtendedParams = {}) => {
174+
<const ContextName extends string>(contextName: ContextName, subContextExtendedParams: ExtendedParams) =>
175+
<const ChildContextName extends string>(
176+
childContextName: ChildContextName,
177+
extendedParams: ExtendedParams = {}
178+
) => {
167179
const subErrorContext = { ...subContextExtendedParams, ...extendedParams };
168180
return _createErrorContext(`${contextName}/${childContextName}`, subErrorContext);
169181
};
170182

171-
function _createErrorContext(
172-
_contextName: string,
183+
function _createErrorContext<const ContextName extends string>(
184+
_contextName: ContextName,
173185
contextExtendedParams: ExtendedParams = outerExtendedParams
174-
): ErrorSubcontext<ErrorTypes[number]["errorType"]> {
186+
): ErrorSubcontext<ContextName, ErrorTypes[number]["errorType"]> {
175187
return {
188+
__brand: _contextName,
176189
subcontext: _createSubcontext(_contextName, contextExtendedParams),
177-
feature: (childFeatureName: string, extendedParams: ExtendedParams = {}) => {
190+
feature: <const FeatureName extends string>(
191+
childFeatureName: FeatureName,
192+
extendedParams: ExtendedParams = {}
193+
) => {
178194
const featureErrorContext = { ...contextExtendedParams, ...extendedParams };
179195
return _createErrorFeature(childFeatureName, _contextName, featureErrorContext);
180196
},
181197
};
182198
}
183199

184-
function _createErrorFeature(
185-
featureName: string,
186-
contextName: string,
200+
function _createErrorFeature<const ContextName extends string, const FeatureName extends string>(
201+
featureName: FeatureName,
202+
contextName: ContextName,
187203
featureContextExtendedParams: ExtendedParams = {}
188-
) {
204+
): ErrorFeature<`${ContextName}/${FeatureName}`, ErrorTypes[number]["errorType"]> {
189205
const createNewErrorObject: CreateErrorFn<ErrorTypes[number]["errorType"]> = (
190206
errorType,
191207
message: string,
@@ -215,7 +231,8 @@ export function createError<ErrorTypes extends ErrorTypeConfig>(errorTypes?: Err
215231
return error;
216232
};
217233

218-
return createNewErrorObject;
234+
Object.assign(createNewErrorObject, { __brand: `${contextName}/${featureName}` as const });
235+
return createNewErrorObject as ErrorFeature<`${ContextName}/${FeatureName}`, ErrorTypes[number]["errorType"]>;
219236
}
220237

221238
return _createErrorContext(contextName);

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "conway-errors",
33
"source": "index.ts",
4-
"version": "3.1.2",
4+
"version": "3.2.0",
55
"private": false,
6-
"description": "Create errors with Conway's law",
6+
"description": "Simplify the creation, structuring and throwing of errors",
77
"exports": {
88
"types": "./dist/index.d.ts",
99
"require": "./dist/index.js",
@@ -31,8 +31,19 @@
3131
"/dist",
3232
"/package.json"
3333
],
34-
"keywords": [],
35-
"author": "Kalagin Ivan",
34+
"keywords": [
35+
"errors",
36+
"typescript",
37+
"error-handling",
38+
"custom-errors",
39+
"structured-errors",
40+
"error-utils",
41+
"backend",
42+
"exceptions",
43+
"js-errors",
44+
"application-error"
45+
],
46+
"author": "ivklgn",
3647
"license": "MIT",
3748
"devDependencies": {
3849
"@biomejs/biome": "1.8.3",

0 commit comments

Comments
 (0)