From be466ec5dcb73ebcf374aa163560d4faa5419770 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Wed, 8 Apr 2026 15:12:02 +0300 Subject: [PATCH 1/3] Typed params validation in nested blocks --- docs/typescript-examples/object.ts | 19 ++----------------- lib/functionBlock.ts | 15 +++++++++++++-- lib/index.ts | 8 ++++---- lib/types.ts | 7 +++++++ 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/docs/typescript-examples/object.ts b/docs/typescript-examples/object.ts index d5ed514..ac7bfc1 100644 --- a/docs/typescript-examples/object.ts +++ b/docs/typescript-examples/object.ts @@ -96,7 +96,7 @@ const block2 = de.http({ const block2Func = de.func({ // eslint-disable-next-line @typescript-eslint/no-unused-vars - block: ({ params }: { params: InferParamsInFromBlock & { p1: number } }) => block2, + block: ({ params }: { params: InferParamsInFromBlock & { p1: number } }) => block2, // block: () => block2, options: { after: ({ result }) => { @@ -111,11 +111,8 @@ const block2Func = de.func({ de.run(block2Func, { params: { - id1: '67890', p1: 1, - payload: { - card: {}, - }, + id2: 578923, }, }) .then((result) => { @@ -166,12 +163,8 @@ const block3 = de.object({ de.run(block3, { params: { - id1: '12345', id2: 67890, p1: 1, - payload: { - card: {}, - }, }, }) .then((result) => { @@ -230,12 +223,8 @@ const block5 = block3.extend({ de.run(block4, { params: { - id1: '12345', id2: 67890, p1: 1, - payload: { - card: {}, - }, }, }) .then((result) => { @@ -244,12 +233,8 @@ de.run(block4, { de.run(block5, { params: { - id1: '12345', id2: 67890, p1: 1, - payload: { - card: {}, - }, }, }) .then((result) => { diff --git a/lib/functionBlock.ts b/lib/functionBlock.ts index 29234b7..c2b82bb 100644 --- a/lib/functionBlock.ts +++ b/lib/functionBlock.ts @@ -2,10 +2,21 @@ import BaseBlock from './block'; import type { DepAccessor, DescriptBlockDeps } from './depsDomain'; import DepsDomain, { createDepAccessor } from './depsDomain'; import { createError, ERROR_ID } from './error'; -import type { BlockResultOut, DescriptBlockOptions, InferParamsOutFromBlock } from './types'; +import type { BlockResultOut, DescriptBlockOptions, DescriptParamsError, InferParamsOutFromBlock } from './types'; import type ContextClass from './context'; import type Cancel from './cancel'; +type NestedBlockParamsConstraint = + [ unknown ] extends [ Params ] + ? T + : T extends BaseBlock + ? unknown extends BParams + ? T + : [ Params ] extends [ BParams ] + ? T + : DescriptParamsError + : T; + export type FunctionBlockDefinition< Context, Params, @@ -18,7 +29,7 @@ export type FunctionBlockDefinition< generateId: DepsDomain['generateId']; cancel: Cancel; blockCancel: Cancel; -}) => Promise | BlockResult; +}) => Promise> | NestedBlockParamsConstraint; class FunctionBlock< Context, diff --git a/lib/index.ts b/lib/index.ts index b87505a..6ea1fa1 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -68,7 +68,7 @@ const array = function< Params = GetArrayBlockParams, >({ block, options }: { block: ArrayBlockDefinition; - options?: DescriptBlockOptions; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; }) { return new ArrayBlock({ block, options }); }; @@ -85,7 +85,7 @@ const object = function< Params = GetObjectBlockParams, >({ block, options }: { block?: ObjectBlockDefinition; - options?: DescriptBlockOptions; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; } = {}) { return new ObjectBlock({ block, options }); }; @@ -121,7 +121,7 @@ const first = function< Params = GetFirstBlockParams, >({ block, options }: { block: FirstBlockDefinition; - options?: DescriptBlockOptions; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; }) { return new FirstBlock({ block, options }); }; @@ -138,7 +138,7 @@ const pipe = function< Params = GetPipeBlockParams, >({ block, options }: { block: PipeBlockDefinition; - options?: DescriptBlockOptions; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; }) { return new PipeBlock({ block, options }); }; diff --git a/lib/types.ts b/lib/types.ts index 57aea6c..47124a7 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -41,6 +41,13 @@ export type NonNullableObject> = { [P in keyof T]: Exclude; }; +export type DescriptParamsError = { + readonly __descriptError: 'NESTED_BLOCK_PARAMS_INCOMPATIBLE'; + readonly __requiredParams: Required; + readonly __availableParams: Available; + readonly __fix: 'Add options.params to transform parent params into the required shape'; +}; + export type DescriptJSON = boolean | number | From b946cd34c64fb7bd8809a264b2dac043b3820cb7 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Tue, 14 Apr 2026 12:45:33 +0300 Subject: [PATCH 2/3] fix rest cases --- lib/functionBlock.ts | 15 ++------------- lib/index.ts | 3 ++- lib/types.ts | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/functionBlock.ts b/lib/functionBlock.ts index c2b82bb..29234b7 100644 --- a/lib/functionBlock.ts +++ b/lib/functionBlock.ts @@ -2,21 +2,10 @@ import BaseBlock from './block'; import type { DepAccessor, DescriptBlockDeps } from './depsDomain'; import DepsDomain, { createDepAccessor } from './depsDomain'; import { createError, ERROR_ID } from './error'; -import type { BlockResultOut, DescriptBlockOptions, DescriptParamsError, InferParamsOutFromBlock } from './types'; +import type { BlockResultOut, DescriptBlockOptions, InferParamsOutFromBlock } from './types'; import type ContextClass from './context'; import type Cancel from './cancel'; -type NestedBlockParamsConstraint = - [ unknown ] extends [ Params ] - ? T - : T extends BaseBlock - ? unknown extends BParams - ? T - : [ Params ] extends [ BParams ] - ? T - : DescriptParamsError - : T; - export type FunctionBlockDefinition< Context, Params, @@ -29,7 +18,7 @@ export type FunctionBlockDefinition< generateId: DepsDomain['generateId']; cancel: Cancel; blockCancel: Cancel; -}) => Promise> | NestedBlockParamsConstraint; +}) => Promise | BlockResult; class FunctionBlock< Context, diff --git a/lib/index.ts b/lib/index.ts index 6ea1fa1..a1c745d 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -28,6 +28,7 @@ import type { InferBlock, InferHttpBlock, InferResultOrResult, + ExtractBadNestedParams, } from './types'; import type BaseBlock from './block'; import type { DescriptHttpBlockDescription, DescriptHttpBlockQuery, DescriptHttpBlockQueryValue } from './httpBlock'; @@ -51,7 +52,7 @@ const func = function< options?: DescriptBlockOptions< Context, ParamsOut, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params >; -}) { +} & ([ ExtractBadNestedParams ] extends [ never ] ? unknown : ExtractBadNestedParams)) { return new FunctionBlock< Context, ParamsOut, BlockResult, ResultOut, BeforeResultOut, AfterResultOut, ErrorResultOut, Params >({ block, options }); diff --git a/lib/types.ts b/lib/types.ts index 47124a7..4700502 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -48,6 +48,20 @@ export type DescriptParamsError = { readonly __fix: 'Add options.params to transform parent params into the required shape'; }; +// Extracts DescriptParamsError for any block in T whose Params are not satisfied by the available Params. +// Returns never when all blocks are compatible (no constraint added to the call site). +export type ExtractBadNestedParams = + [ unknown ] extends [ Params ] + ? never + // eslint-disable-next-line @typescript-eslint/no-explicit-any + : T extends BaseBlock + ? unknown extends BParams + ? never + : [ Params ] extends [ BParams ] + ? never + : DescriptParamsError + : never; + export type DescriptJSON = boolean | number | From 069e866d3d4f3b908914607bfa12ad1fb08b36bb Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Thu, 16 Apr 2026 14:57:43 +0300 Subject: [PATCH 3/3] update examples --- docs/typescript-examples/array.ts | 26 ++++++++++++++++++++++++ docs/typescript-examples/first.ts | 26 ++++++++++++++++++++++++ docs/typescript-examples/func.ts | 25 +++++++++++++++++++++++ docs/typescript-examples/object.ts | 26 ++++++++++++++++++++++++ docs/typescript-examples/pipe.ts | 26 ++++++++++++++++++++++++ docs/typescript-examples/shared.ts | 32 ++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 docs/typescript-examples/shared.ts diff --git a/docs/typescript-examples/array.ts b/docs/typescript-examples/array.ts index c75226e..d9ada4a 100644 --- a/docs/typescript-examples/array.ts +++ b/docs/typescript-examples/array.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import * as de from '../../lib'; +import { blockNeedsA, blockNeedsB, ParamsA, ParamsAB } from './shared'; // --------------------------------------------------------------------------------------------------------------- // @@ -155,3 +156,28 @@ de.run(bfn3, { .then((result) => { console.log(result[ 0 ], result[ 1 ]); }); + +// --------------------------------------------------------------------------------------------------------------- // +// Тут проверяем, что есть ошибка при несовпадении параметров и нету ошибки при валидных параметрах вложенных блоков +// @ts-expect-error тут должна быть DescriptParamsError +de.func({ + block: ({ params }: { params: ParamsA }) => { + void params; + + return de.array({ + block: [ blockNeedsA, blockNeedsB ], + }); + }, +}); + +de.func({ + block: ({ params }: { params: ParamsAB }) => { + void params; + + return de.array({ + block: [ blockNeedsA, blockNeedsB ], + }); + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // diff --git a/docs/typescript-examples/first.ts b/docs/typescript-examples/first.ts index 1b6e61f..7a7ef13 100644 --- a/docs/typescript-examples/first.ts +++ b/docs/typescript-examples/first.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import * as de from '../../lib'; +import { blockNeedsA, blockNeedsB, ParamsA, ParamsAB } from './shared'; // --------------------------------------------------------------------------------------------------------------- // @@ -164,3 +165,28 @@ de.run(bfn3, { .then((result) => { console.log(result); }); + +// --------------------------------------------------------------------------------------------------------------- // +// Тут проверяем, что есть ошибка при несовпадении параметров и нету ошибки при валидных параметрах вложенных блоков +// @ts-expect-error тут должна быть DescriptParamsError +de.func({ + block: ({ params }: { params: ParamsA }) => { + void params; + + return de.first({ + block: [ blockNeedsA, blockNeedsB ], + }); + }, +}); + +de.func({ + block: ({ params }: { params: ParamsAB }) => { + void params; + + return de.first({ + block: [ blockNeedsA, blockNeedsB ], + }); + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // diff --git a/docs/typescript-examples/func.ts b/docs/typescript-examples/func.ts index 48c70d4..0ec771a 100644 --- a/docs/typescript-examples/func.ts +++ b/docs/typescript-examples/func.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import * as de from '../../lib'; +import { blockNeedsA, blockNeedsAB, blockNeedsB, ParamsA, ParamsAB } from './shared'; interface ParamsIn1 { id: string; @@ -126,3 +127,27 @@ de.run(block4, { .then((result) => { console.log(result); }); + +// --------------------------------------------------------------------------------------------------------------- // +// Тут проверяем, что есть ошибка при несовпадении параметров и нету ошибки при валидных параметрах вложенных блоков +// @ts-expect-error тут должна быть DescriptParamsError +de.func({ + block: ({ params }: { params: ParamsA }) => + params.a === 'special' ? blockNeedsA : blockNeedsB, +}); + +de.func({ + block: ({ params }: { params: ParamsAB }) => + params.a === 'special' ? blockNeedsA : blockNeedsB, +}); + +de.run(blockNeedsAB, { + params: { a: 'hello', b: 42 }, +}); + +de.run(blockNeedsAB, { + // @ts-expect-error тут должна быть ошибка 'b' is declared here. + params: { a: 'hello' }, +}); + +// --------------------------------------------------------------------------------------------------------------- // diff --git a/docs/typescript-examples/object.ts b/docs/typescript-examples/object.ts index ac7bfc1..1e40123 100644 --- a/docs/typescript-examples/object.ts +++ b/docs/typescript-examples/object.ts @@ -2,6 +2,7 @@ import * as de from '../../lib'; import type { DescriptHttpBlockResult, InferParamsInFromBlock } from '../../lib/types'; +import { blockNeedsA, blockNeedsB, ParamsA, ParamsAB } from './shared'; // --------------------------------------------------------------------------------------------------------------- // @@ -240,3 +241,28 @@ de.run(block5, { .then((result) => { console.log(result); }); + +// --------------------------------------------------------------------------------------------------------------- // +// Тут проверяем, что есть ошибка при несовпадении параметров и нету ошибки при валидных параметрах вложенных блоков +// @ts-expect-error тут должна быть DescriptParamsError +de.func({ + block: ({ params }: { params: ParamsA }) => { + void params; + + return de.object({ + block: { blockNeedsA, blockNeedsB }, + }); + }, +}); + +de.func({ + block: ({ params }: { params: ParamsAB }) => { + void params; + + return de.object({ + block: { blockNeedsA, blockNeedsB }, + }); + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // diff --git a/docs/typescript-examples/pipe.ts b/docs/typescript-examples/pipe.ts index ccd1177..4c8bd34 100644 --- a/docs/typescript-examples/pipe.ts +++ b/docs/typescript-examples/pipe.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import * as de from '../../lib'; +import { blockNeedsA, blockNeedsB, ParamsA, ParamsAB } from './shared'; // --------------------------------------------------------------------------------------------------------------- // @@ -164,3 +165,28 @@ de.run(bfn3, { .then((result) => { console.log(result); }); + +// --------------------------------------------------------------------------------------------------------------- // +// Тут проверяем, что есть ошибка при несовпадении параметров и нету ошибки при валидных параметрах вложенных блоков +// @ts-expect-error тут должна быть DescriptParamsError +de.func({ + block: ({ params }: { params: ParamsA }) => { + void params; + + return de.pipe({ + block: [ blockNeedsA, blockNeedsB ], + }); + }, +}); + +de.func({ + block: ({ params }: { params: ParamsAB }) => { + void params; + + return de.pipe({ + block: [ blockNeedsA, blockNeedsB ], + }); + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // diff --git a/docs/typescript-examples/shared.ts b/docs/typescript-examples/shared.ts new file mode 100644 index 0000000..8eb240e --- /dev/null +++ b/docs/typescript-examples/shared.ts @@ -0,0 +1,32 @@ +import * as de from '../../lib'; + +export interface ParamsA { + a: string; +} + +export interface ParamsB { + b: number; +} + +export interface ParamsAB { + a: string; + b: number; +} + +export const blockNeedsA = de.http({ + block: { + pathname: ({ params }: { params: ParamsA }) => `/a/${ params.a }`, + }, +}); + +export const blockNeedsB = de.http({ + block: { + pathname: ({ params }: { params: ParamsB }) => `/b/${ params.b }`, + }, +}); + +export const blockNeedsAB = de.http({ + block: { + pathname: ({ params }: { params: ParamsAB }) => `/b/${ params.b }/${ params.a }`, + }, +});