From e5cdf0b93e4b6ae69e5b917edac5610ec31bdb6d Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Tue, 21 Apr 2026 14:55:11 +0300 Subject: [PATCH 1/5] required return types attempt 2 new generic --- lib/arrayBlock.ts | 12 +++++++++--- lib/block.ts | 2 ++ lib/compositeBlock.ts | 4 +++- lib/firstBlock.ts | 4 +++- lib/functionBlock.ts | 20 ++++++++++++-------- lib/httpBlock.ts | 4 +++- lib/index.ts | 30 ++++++++++++++++++------------ lib/objectBlock.ts | 8 ++++++-- lib/pipeBlock.ts | 4 +++- lib/types.ts | 12 ++++++++++++ 10 files changed, 71 insertions(+), 29 deletions(-) diff --git a/lib/arrayBlock.ts b/lib/arrayBlock.ts index ccddeab..3f509eb 100644 --- a/lib/arrayBlock.ts +++ b/lib/arrayBlock.ts @@ -15,10 +15,14 @@ import type Cancel from './cancel'; import type { DescriptBlockDeps } from './depsDomain'; import type DepsDomain from './depsDomain'; +type InferArrayElement = B extends { readonly __isRequired: true } + ? InferResultFromBlock + : InferResultFromBlock | DescriptError; + export type GetArrayBlockResult> = { 0: never; - 1: [ InferResultFromBlock> | DescriptError ]; - 2: [ InferResultFromBlock> | DescriptError, ...GetArrayBlockResult> ]; + 1: [ InferArrayElement> ]; + 2: [ InferArrayElement>, ...GetArrayBlockResult> ]; }[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; export type GetArrayBlockParamsUnion> = { @@ -57,6 +61,7 @@ class ArrayBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetArrayBlockParams, + IsRequired extends boolean = boolean, > extends CompositeBlock< Context, ArrayBlockDefinition, @@ -68,7 +73,8 @@ class ArrayBlock< BeforeResultOut, AfterResultOut, ErrorResultOut, - Params + Params, + IsRequired > { extend< diff --git a/lib/block.ts b/lib/block.ts index 90982a8..b877902 100644 --- a/lib/block.ts +++ b/lib/block.ts @@ -41,8 +41,10 @@ abstract class BaseBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = ParamsOut, + IsRequired extends boolean = boolean, > { declare readonly __resultType: InferResultOrResult; + declare readonly __isRequired: IsRequired; protected block: CustomBlock; protected options: BlockOptions; diff --git a/lib/compositeBlock.ts b/lib/compositeBlock.ts index 3c1f936..cb0e638 100644 --- a/lib/compositeBlock.ts +++ b/lib/compositeBlock.ts @@ -28,6 +28,7 @@ abstract class CompositeBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = ParamsOut, + IsRequired extends boolean = boolean, > extends BaseBlock< Context, CustomBlock, @@ -39,7 +40,8 @@ abstract class CompositeBlock< BeforeResultOut, AfterResultOut, ErrorResultOut, - Params + Params, + IsRequired > { protected blocks: Array<{ diff --git a/lib/firstBlock.ts b/lib/firstBlock.ts index 24e2db8..97a2a83 100644 --- a/lib/firstBlock.ts +++ b/lib/firstBlock.ts @@ -68,6 +68,7 @@ class FirstBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetFirstBlockParams, + IsRequired extends boolean = boolean, > extends CompositeBlock< Context, FirstBlockDefinition, @@ -79,7 +80,8 @@ class FirstBlock< BeforeResultOut, AfterResultOut, ErrorResultOut, - Params + Params, + IsRequired > { extend< diff --git a/lib/functionBlock.ts b/lib/functionBlock.ts index 29234b7..878735b 100644 --- a/lib/functionBlock.ts +++ b/lib/functionBlock.ts @@ -29,6 +29,7 @@ class FunctionBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = never extends InferParamsOutFromBlock ? ParamsOut : InferParamsOutFromBlock, + IsRequired extends boolean = boolean, > extends BaseBlock< Context, FunctionBlockDefinition, @@ -40,7 +41,8 @@ class FunctionBlock< BeforeResultOut, AfterResultOut, ErrorResultOut, - Params + Params, + IsRequired > { protected initBlock(block: FunctionBlockDefinition) { @@ -104,6 +106,7 @@ class FunctionBlock< ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, + const ExtendedIsRequired extends boolean = IsRequired, >({ options }: { options: DescriptBlockOptions< Context, @@ -113,14 +116,15 @@ class FunctionBlock< ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams - >; + > & { required?: ExtendedIsRequired }; }) { - return new FunctionBlock({ - block: this.extendBlock(this.block) as typeof this.block, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - options: this.extendOptions(this.options, options) as typeof options, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = new FunctionBlock({ block: this.extendBlock(this.block) as any, options: this.extendOptions(this.options, options) as any }); + return result as unknown as FunctionBlock< + Context, ExtendedParamsOut, ExtendedBlockResult, + BlockResultOut, + ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams, ExtendedIsRequired + >; } } diff --git a/lib/httpBlock.ts b/lib/httpBlock.ts index 27a3007..a670fb2 100644 --- a/lib/httpBlock.ts +++ b/lib/httpBlock.ts @@ -188,6 +188,7 @@ class HttpBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = ParamsOut, + IsRequired extends boolean = boolean, > extends Block< Context, DescriptHttpBlockDescription, @@ -198,7 +199,8 @@ class HttpBlock< BeforeResultOut, AfterResultOut, ErrorResultOut, - Params + Params, + IsRequired > { extend< diff --git a/lib/index.ts b/lib/index.ts index a1c745d..cdf9fd6 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -47,14 +47,15 @@ const func = function< AfterResultOut = unknown, ErrorResultOut = unknown, Params = ParamsOut, + const IsRequired extends boolean = false, >({ block, options }: { block: FunctionBlockDefinition; options?: DescriptBlockOptions< Context, ParamsOut, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params - >; + > & { required?: IsRequired }; } & ([ ExtractBadNestedParams ] extends [ never ] ? unknown : ExtractBadNestedParams)) { return new FunctionBlock< - Context, ParamsOut, BlockResult, ResultOut, BeforeResultOut, AfterResultOut, ErrorResultOut, Params + Context, ParamsOut, BlockResult, ResultOut, BeforeResultOut, AfterResultOut, ErrorResultOut, Params, IsRequired >({ block, options }); }; const array = function< @@ -67,11 +68,12 @@ const array = function< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetArrayBlockParams, + const IsRequired extends boolean = false, >({ block, options }: { block: ArrayBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required?: IsRequired }; }) { - return new ArrayBlock({ block, options }); + return new ArrayBlock({ block, options }); }; const object = function< Context, @@ -84,11 +86,12 @@ const object = function< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetObjectBlockParams, + const IsRequired extends boolean = false, >({ block, options }: { block?: ObjectBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required?: IsRequired }; } = {}) { - return new ObjectBlock({ block, options }); + return new ObjectBlock({ block, options }); }; const http = function< Context, @@ -101,12 +104,13 @@ const http = function< AfterResultOut = unknown, ErrorResultOut = unknown, Params = ParamsOut, + const IsRequired extends boolean = false, >({ block, options }: { block?: DescriptHttpBlockDescription; - options?: DescriptBlockOptions; + options?: DescriptBlockOptions & { required?: IsRequired }; }) { return new HttpBlock< - Context, ParamsOut, IntermediateResult, ResultOut, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params + Context, ParamsOut, IntermediateResult, ResultOut, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params, IsRequired >({ block, options }); }; @@ -120,11 +124,12 @@ const first = function< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetFirstBlockParams, + const IsRequired extends boolean = false, >({ block, options }: { block: FirstBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required?: IsRequired }; }) { - return new FirstBlock({ block, options }); + return new FirstBlock({ block, options }); }; const pipe = function< @@ -137,11 +142,12 @@ const pipe = function< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetPipeBlockParams, + const IsRequired extends boolean = false, >({ block, options }: { block: PipeBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required?: IsRequired }; }) { - return new PipeBlock({ block, options }); + return new PipeBlock({ block, options }); }; const isBlock = function(block: any) { diff --git a/lib/objectBlock.ts b/lib/objectBlock.ts index 5e7500e..ce3ed70 100644 --- a/lib/objectBlock.ts +++ b/lib/objectBlock.ts @@ -18,7 +18,9 @@ export type InferResultFromObjectBlocks = Block extends BaseBlock< Block; export type GetObjectBlockResult> = { - [ P in keyof T ]: InferResultFromBlock | DescriptError + [ P in keyof T ]: T[P] extends { readonly __isRequired: true } + ? InferResultFromBlock + : InferResultFromBlock | DescriptError }; export type GetObjectBlockParams< @@ -50,6 +52,7 @@ class ObjectBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetObjectBlockParams, + IsRequired extends boolean = boolean, > extends CompositeBlock< Context, ObjectBlockDefinition, @@ -61,7 +64,8 @@ class ObjectBlock< BeforeResultOut, AfterResultOut, ErrorResultOut, - Params + Params, + IsRequired > { protected initBlock(object: ObjectBlockDefinition) { diff --git a/lib/pipeBlock.ts b/lib/pipeBlock.ts index e9a607f..6ba990e 100644 --- a/lib/pipeBlock.ts +++ b/lib/pipeBlock.ts @@ -68,6 +68,7 @@ class PipeBlock< AfterResultOut = unknown, ErrorResultOut = unknown, Params = GetPipeBlockParams, + IsRequired extends boolean = boolean, > extends CompositeBlock< Context, PipeBlockDefinition, @@ -79,7 +80,8 @@ class PipeBlock< BeforeResultOut, AfterResultOut, ErrorResultOut, - Params + Params, + IsRequired > { extend< diff --git a/lib/types.ts b/lib/types.ts index 4700502..306fbda 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -111,6 +111,18 @@ export type InferResultFromBlock = Type extends BaseBlock< infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params > ? InferResultOrResult : never; +export type DeepResolveResult = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends BaseBlock + ? DeepResolveResult> + : T extends DescriptError + ? DescriptError + : T extends Record + ? { [K in keyof T]: DeepResolveResult } + : T; + +export type DeepInferResultFromBlock = DeepResolveResult>; + export type InferParamsInFromBlock = Type extends BaseBlock< // eslint-disable-next-line @typescript-eslint/no-unused-vars infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, From 409c526c82c75e0ac70cf937aa8fcc178828f74b Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Wed, 22 Apr 2026 12:34:56 +0300 Subject: [PATCH 2/5] IsRequiredBlock --- lib/arrayBlock.ts | 3 ++- lib/objectBlock.ts | 4 ++-- lib/types.ts | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/arrayBlock.ts b/lib/arrayBlock.ts index 3f509eb..fb6eacf 100644 --- a/lib/arrayBlock.ts +++ b/lib/arrayBlock.ts @@ -6,6 +6,7 @@ import type { First, InferResultFromBlock, InferParamsInFromBlock, + IsRequiredBlock, Tail, DescriptBlockOptions, } from './types'; @@ -15,7 +16,7 @@ import type Cancel from './cancel'; import type { DescriptBlockDeps } from './depsDomain'; import type DepsDomain from './depsDomain'; -type InferArrayElement = B extends { readonly __isRequired: true } +type InferArrayElement = B extends IsRequiredBlock ? InferResultFromBlock : InferResultFromBlock | DescriptError; diff --git a/lib/objectBlock.ts b/lib/objectBlock.ts index ce3ed70..fad2985 100644 --- a/lib/objectBlock.ts +++ b/lib/objectBlock.ts @@ -2,7 +2,7 @@ import CompositeBlock from './compositeBlock'; import type { DescriptError } from './error'; import { createError, ERROR_ID } from './error'; import type BaseBlock from './block'; -import type { InferParamsInFromBlock, DescriptBlockOptions, BlockResultOut, UnionToIntersection, InferResultFromBlock } from './types'; +import type { InferParamsInFromBlock, DescriptBlockOptions, BlockResultOut, UnionToIntersection, InferResultFromBlock, IsRequiredBlock } from './types'; import type ContextClass from './context'; import type Cancel from './cancel'; import type { DescriptBlockDeps } from './depsDomain'; @@ -18,7 +18,7 @@ export type InferResultFromObjectBlocks = Block extends BaseBlock< Block; export type GetObjectBlockResult> = { - [ P in keyof T ]: T[P] extends { readonly __isRequired: true } + [ P in keyof T ]: T[P] extends IsRequiredBlock ? InferResultFromBlock : InferResultFromBlock | DescriptError }; diff --git a/lib/types.ts b/lib/types.ts index 306fbda..8d3d496 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -104,6 +104,8 @@ export type InferResultOrResultOnce = Result extends BaseBlock< infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params > ? ResultOut : Result; +export type IsRequiredBlock = { readonly __isRequired: true }; + export type InferResultFromBlock = Type extends BaseBlock< // eslint-disable-next-line @typescript-eslint/no-unused-vars infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, From 018a1a7cf1f372303ca94fb4d391bc7ea5d001da Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Wed, 22 Apr 2026 12:43:53 +0300 Subject: [PATCH 3/5] tests --- tests/options.required.test.ts | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/options.required.test.ts b/tests/options.required.test.ts index cb51c03..de1ae2e 100644 --- a/tests/options.required.test.ts +++ b/tests/options.required.test.ts @@ -6,6 +6,7 @@ import * as de from '../lib'; import { DescriptError } from '../lib'; import { getPath } from './helpers'; import Server from './server'; +import type { Expect, TypesMatch } from './test.types'; const PORT = 10000; const fake = new Server({ @@ -180,4 +181,55 @@ describe('options.required', () => { const childHeaders = spy.mock.calls[ 0 ][ 0 ].headers; expect(childHeaders[ 'x-required-header' ]).toBe('true'); }); + + it('types: required=true removes DescriptError from object block result', async() => { + const block = de.object({ + block: { + foo: de.func({ + block: () => 42, + options: { + required: true, + }, + }), + bar: de.func({ + block: () => 'hello', + options: { + required: true, + }, + }), + }, + }); + + const result = await de.run(block, { params: {} }); + + expect(result.foo).toBe(42); + expect(result.bar).toBe('hello'); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type Tests = [ + Expect>, + ]; + }); + + it('types: required=false keeps DescriptError in object block result', async() => { + const block = de.object({ + block: { + foo: de.func({ + block: () => 42, + options: { + required: false, + }, + }), + }, + }); + + const result = await de.run(block, { params: {} }); + + expect(result.foo).toBe(42); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type Tests = [ + Expect>, + ]; + }); }); From 488c6c0e1d52f01bae721b8ea14425ef550daf39 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Wed, 22 Apr 2026 12:50:06 +0300 Subject: [PATCH 4/5] fix pipe block result types --- lib/pipeBlock.ts | 11 ++--------- lib/types.ts | 4 ++++ tests/pipeBlock.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/pipeBlock.ts b/lib/pipeBlock.ts index 6ba990e..def6553 100644 --- a/lib/pipeBlock.ts +++ b/lib/pipeBlock.ts @@ -1,10 +1,10 @@ import CompositeBlock from './compositeBlock'; import { ERROR_ID, createError } from './error'; -import type { DescriptError } from './error'; import type BaseBlock from './block'; import type { BlockResultOut, First, + Last, InferResultFromBlock, InferParamsInFromBlock, Tail, @@ -32,12 +32,6 @@ export type GetPipeBlockParams< PU = GetPipeBlockParamsUnion, > = PU; -type GetPipeBlockResultUnion> = { - 0: never; - 1: First | DescriptError; - 2: First | DescriptError | GetPipeBlockResultUnion>; -}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; - type GetPipeBlockResultMap> = { [ P in keyof T ]: InferResultFromBlock; }; @@ -45,8 +39,7 @@ type GetPipeBlockResultMap> = { export type GetPipeBlockResult< T extends ReadonlyArray, PA extends ReadonlyArray = GetPipeBlockResultMap, - PU = GetPipeBlockResultUnion, -> = PU; +> = Last; export type PipeBlockDefinition = { [ P in keyof T ]: T[ P ] extends BaseBlock< diff --git a/lib/types.ts b/lib/types.ts index 8d3d496..bad2d8f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -175,6 +175,10 @@ export type Tail = // eslint-disable-next-line @typescript-eslint/no-unused-vars T extends readonly [ infer First, ...infer Rest ] | [ infer First, ...infer Rest ] ? Rest : never; +export type Last = +// eslint-disable-next-line @typescript-eslint/no-unused-vars + T extends readonly [ ...infer Rest, infer L ] | [ ...infer Rest, infer L ] ? L : never; + export type Equal = A extends B ? (B extends A ? A : never) : never; export type DepsIds = Array | UntypedId>; diff --git a/tests/pipeBlock.test.ts b/tests/pipeBlock.test.ts index 6850785..b6d661c 100644 --- a/tests/pipeBlock.test.ts +++ b/tests/pipeBlock.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as de from '../lib'; +import type { Expect, TypesMatch } from './test.types'; describe('de.pipe', () => { @@ -115,4 +116,29 @@ describe('de.pipe', () => { } }); + it('types: result type is the last block result type', async() => { + const block1 = de.func({ + block: () => 42, + }); + const block2 = de.func({ + block: () => 'hello', + }); + const block3 = de.func({ + block: () => true, + }); + + const block = de.pipe({ + block: [ block1, block2, block3 ], + }); + + const result = await de.run(block, { params: {} }); + + expect(typeof result).toBe('boolean'); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type Tests = [ + Expect>, + ]; + }); + }); From e6992f11b7a3b6fbbd3885b52d6ca38ce9c0af2a Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Wed, 22 Apr 2026 12:56:29 +0300 Subject: [PATCH 5/5] fix ts --- docs/typescript-examples/pipe.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/typescript-examples/pipe.ts b/docs/typescript-examples/pipe.ts index 4c8bd34..5c90488 100644 --- a/docs/typescript-examples/pipe.ts +++ b/docs/typescript-examples/pipe.ts @@ -67,13 +67,7 @@ const block3 = de.pipe({ return params; }, after: ({ result }) => { - if (typeof result === 'number') { - return result; - } else if ('b1' in result) { - return result.b1; - } else if ('b2' in result) { - return result.b2; - } + return result * 2; }, }, });