From b101cefbb356fe40e76daaa34ab86d6870379de2 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Fri, 10 Apr 2026 12:06:30 +0300 Subject: [PATCH 1/5] added required params fix --- lib/arrayBlock.ts | 45 +++++-- lib/block.ts | 41 ++++--- lib/firstBlock.ts | 56 +++++---- lib/functionBlock.ts | 43 ++++--- lib/httpBlock.ts | 51 +++++--- lib/index.ts | 281 ++++++++++++++++++++++++++++--------------- lib/objectBlock.ts | 43 +++++-- lib/pipeBlock.ts | 54 +++++---- lib/types.ts | 5 + 9 files changed, 408 insertions(+), 211 deletions(-) diff --git a/lib/arrayBlock.ts b/lib/arrayBlock.ts index ccddeab..2ba209c 100644 --- a/lib/arrayBlock.ts +++ b/lib/arrayBlock.ts @@ -1,10 +1,9 @@ import CompositeBlock from './compositeBlock'; import { createError, ERROR_ID } from './error'; -import type { DescriptError } from './error'; import type { BlockResultOut, First, - InferResultFromBlock, + InferResultOrError, InferParamsInFromBlock, Tail, DescriptBlockOptions, @@ -17,8 +16,8 @@ import type DepsDomain from './depsDomain'; export type GetArrayBlockResult> = { 0: never; - 1: [ InferResultFromBlock> | DescriptError ]; - 2: [ InferResultFromBlock> | DescriptError, ...GetArrayBlockResult> ]; + 1: [ InferResultOrError> ]; + 2: [ InferResultOrError>, ...GetArrayBlockResult> ]; }[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; export type GetArrayBlockParamsUnion> = { @@ -72,20 +71,42 @@ class ArrayBlock< > { extend< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ExtendedResultOut extends - BlockResultOut, + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >(args: { + options: DescriptBlockOptions & { required: true }; + }): ArrayBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >( + this: ArrayBlock & { readonly __isRequired: true }, + args: { + options?: DescriptBlockOptions & { required?: true }; + } + ): ArrayBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, - >({ options }: { - options: DescriptBlockOptions< - Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams - >; - }) { + >(args: { + options?: DescriptBlockOptions; + }): ArrayBlock; + extend({ options }: { options?: any }): any { return new ArrayBlock({ block: this.extendBlock(this.block), options: this.extendOptions(this.options, options) as typeof options, diff --git a/lib/block.ts b/lib/block.ts index 90982a8..4d8390f 100644 --- a/lib/block.ts +++ b/lib/block.ts @@ -66,27 +66,40 @@ abstract class BaseBlock< } abstract extend< - - // ExtendedResultOut extends - // BlockResultOut, ExtendedParamsOut extends Params = Params, ExtendedParams = Params, - // ExtendedCustomBlock = CustomBlock, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, - >({ block, options }: { + >(args: { block?: CustomBlock; - options?: DescriptBlockOptions< - Context, - ParamsOut & ExtendedParamsOut, - ExtendedBlockResult, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >; + options: DescriptBlockOptions & { required: true }; + }): unknown; + abstract extend< + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >( + this: BaseBlock & { readonly __isRequired: true }, + args: { + block?: CustomBlock; + options?: DescriptBlockOptions & { required?: true }; + } + ): unknown; + abstract extend< + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >(args: { + block?: CustomBlock; + options?: DescriptBlockOptions; }): unknown; protected initBlock(block: CustomBlock) { diff --git a/lib/firstBlock.ts b/lib/firstBlock.ts index 24e2db8..cfb693c 100644 --- a/lib/firstBlock.ts +++ b/lib/firstBlock.ts @@ -5,7 +5,7 @@ import type BaseBlock from './block'; import type { BlockResultOut, First, - InferResultFromBlock, + InferResultOrError, InferParamsInFromBlock, Tail, DescriptBlockOptions, @@ -32,21 +32,14 @@ export type GetFirstBlockParams< PU = GetFirstBlockParamsUnion, > = PU; -type GetFirstBlockResultMap> = { - [ P in keyof T ]: InferResultFromBlock; -}; - type GetFirstBlockResultUnion> = { 0: never; - 1: First | DescriptError; - 2: First | DescriptError | GetFirstBlockResultUnion>; + 1: InferResultOrError>; + 2: InferResultOrError> | GetFirstBlockResultUnion>; }[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; -export type GetFirstBlockResult< - T extends ReadonlyArray, - PA extends ReadonlyArray = GetFirstBlockResultMap, - PU = GetFirstBlockResultUnion, -> = PU; +export type GetFirstBlockResult> = + GetFirstBlockResultUnion; export type FirstBlockDefinition = { [ P in keyof T ]: T[ P ] extends BaseBlock< @@ -83,21 +76,42 @@ class FirstBlock< > { extend< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ExtendedResultOut extends - BlockResultOut, - // ExtendedCustomBlock extends FirstBlockDefinition, + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >(args: { + options: DescriptBlockOptions & { required: true }; + }): FirstBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >( + this: FirstBlock & { readonly __isRequired: true }, + args: { + options?: DescriptBlockOptions & { required?: true }; + } + ): FirstBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, - >({ options }: { - options: DescriptBlockOptions< - Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams - >; - }) { + >(args: { + options?: DescriptBlockOptions; + }): FirstBlock; + extend({ options }: { options?: any }): any { return new FirstBlock({ block: this.extendBlock(this.block), options: this.extendOptions(this.options, options) as typeof options, diff --git a/lib/functionBlock.ts b/lib/functionBlock.ts index 29234b7..c53d75d 100644 --- a/lib/functionBlock.ts +++ b/lib/functionBlock.ts @@ -97,28 +97,39 @@ class FunctionBlock< extend< ExtendedParamsOut extends Params = Params, ExtendedParams = Params, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // ExtendedCustomBlock = DescriptHttpBlockDescription, - ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, - >({ options }: { - options: DescriptBlockOptions< - Context, - ExtendedParamsOut, - ExtendedBlockResult, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >; - }) { + >(args: { + options: DescriptBlockOptions & { required: true }; + }): FunctionBlock, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams> & { readonly __isRequired: true }; + extend< + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >( + this: FunctionBlock & { readonly __isRequired: true }, + args: { + options?: DescriptBlockOptions & { required?: true }; + } + ): FunctionBlock, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams> & { readonly __isRequired: true }; + extend< + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >(args: { + options?: DescriptBlockOptions; + }): FunctionBlock, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams>; + extend({ options }: { options?: any }): any { 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, }); } diff --git a/lib/httpBlock.ts b/lib/httpBlock.ts index 27a3007..c0958fd 100644 --- a/lib/httpBlock.ts +++ b/lib/httpBlock.ts @@ -205,30 +205,43 @@ class HttpBlock< ExtendedResultOut extends BlockResultOut, ExtendedParamsOut extends Params = Params, ExtendedParams = Params, - - // ExtendedCustomBlock = DescriptHttpBlockDescription, - ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, - >({ options, block }: { + >(args: { block?: DescriptHttpBlockDescription; - options?: DescriptBlockOptions< - Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams - >; - }) { - const x = new HttpBlock< - Context, - ExtendedParamsOut, - HttpResult, - ExtendedResultOut, - ExtendedBlockResult, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >({ + options: DescriptBlockOptions & { required: true }; + }): HttpBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >( + this: HttpBlock & { readonly __isRequired: true }, + args: { + block?: DescriptHttpBlockDescription; + options?: DescriptBlockOptions & { required?: true }; + } + ): HttpBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >(args: { + block?: DescriptHttpBlockDescription; + options?: DescriptBlockOptions; + }): HttpBlock; + extend({ options, block }: { block?: any; options?: any }): any { + const x = new HttpBlock({ block: this.extendBlock(block), options: this.extendOptions(this.options, options), }); diff --git a/lib/index.ts b/lib/index.ts index a1c745d..e9d7108 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -38,110 +38,197 @@ import type { GetFirstBlockParams, GetFirstBlockResult, FirstBlockDefinition } f import type { GetPipeBlockParams, GetPipeBlockResult, PipeBlockDefinition } from './pipeBlock'; import PipeBlock from './pipeBlock'; -const func = function< - Context, - ParamsOut, - BlockResult, - ResultOut extends BlockResultOut, - BeforeResultOut = unknown, - AfterResultOut = unknown, - ErrorResultOut = unknown, - Params = ParamsOut, ->({ block, options }: { - block: FunctionBlockDefinition; - 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 }); +const func: { + < + Context, + ParamsOut, + BlockResult, + ResultOut extends BlockResultOut, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = ParamsOut, + >(args: { + block: FunctionBlockDefinition; + options: DescriptBlockOptions & { required: true }; + } & ([ ExtractBadNestedParams ] extends [ never ] ? unknown : ExtractBadNestedParams)): FunctionBlock & { readonly __isRequired: true }; + < + Context, + ParamsOut, + BlockResult, + ResultOut extends BlockResultOut, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = ParamsOut, + >(args: { + block: FunctionBlockDefinition; + options?: DescriptBlockOptions; + } & ([ ExtractBadNestedParams ] extends [ never ] ? unknown : ExtractBadNestedParams)): FunctionBlock; +} = function({ block, options }: any): any { + return new FunctionBlock({ block, options }); }; -const array = function< - Context, - Block extends ReadonlyArray, - ResultOut extends BlockResultOut, - ParamsOut = GetArrayBlockParams, - BlockResult = GetArrayBlockResult, - BeforeResultOut = unknown, - AfterResultOut = unknown, - ErrorResultOut = unknown, - Params = GetArrayBlockParams, ->({ block, options }: { - block: ArrayBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; -}) { - return new ArrayBlock({ block, options }); +const array: { + < + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetArrayBlockParams, + BlockResult = GetArrayBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetArrayBlockParams, + >(args: { + block: ArrayBlockDefinition; + options: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required: true }; + }): ArrayBlock & { readonly __isRequired: true }; + < + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetArrayBlockParams, + BlockResult = GetArrayBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetArrayBlockParams, + >(args: { + block: ArrayBlockDefinition; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + }): ArrayBlock; +} = function({ block, options }: any): any { + return new ArrayBlock({ block, options }); }; -const object = function< - Context, - Blocks extends Record, - ResultOut extends BlockResultOut, - ParamsOut = GetObjectBlockParams, - BlockResult = GetObjectBlockResult, - - BeforeResultOut = unknown, - AfterResultOut = unknown, - ErrorResultOut = unknown, - Params = GetObjectBlockParams, ->({ block, options }: { - block?: ObjectBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; -} = {}) { - return new ObjectBlock({ block, options }); +const object: { + < + Context, + Blocks extends Record, + ResultOut extends BlockResultOut, + ParamsOut = GetObjectBlockParams, + BlockResult = GetObjectBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetObjectBlockParams, + >(args: { + block?: ObjectBlockDefinition; + options: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required: true }; + }): ObjectBlock & { readonly __isRequired: true }; + < + Context, + Blocks extends Record, + ResultOut extends BlockResultOut, + ParamsOut = GetObjectBlockParams, + BlockResult = GetObjectBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetObjectBlockParams, + >(args?: { + block?: ObjectBlockDefinition; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + }): ObjectBlock; +} = function({ block, options }: any = {}): any { + return new ObjectBlock({ block, options }); }; -const http = function< - Context, - ParamsOut, - ResultOut extends BlockResultOut, - IntermediateResult, - BlockResult extends DescriptHttpBlockResult, - - BeforeResultOut = unknown, - AfterResultOut = unknown, - ErrorResultOut = unknown, - Params = ParamsOut, ->({ block, options }: { - block?: DescriptHttpBlockDescription; - options?: DescriptBlockOptions; -}) { - return new HttpBlock< - Context, ParamsOut, IntermediateResult, ResultOut, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params - >({ block, options }); +const http: { + < + Context, + ParamsOut, + ResultOut extends BlockResultOut, + IntermediateResult, + BlockResult extends DescriptHttpBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = ParamsOut, + >(args: { + block?: DescriptHttpBlockDescription; + options: DescriptBlockOptions & { required: true }; + }): HttpBlock & { readonly __isRequired: true }; + < + Context, + ParamsOut, + ResultOut extends BlockResultOut, + IntermediateResult, + BlockResult extends DescriptHttpBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = ParamsOut, + >(args: { + block?: DescriptHttpBlockDescription; + options?: DescriptBlockOptions; + }): HttpBlock; +} = function({ block, options }: any): any { + return new HttpBlock({ block, options }); }; -const first = function< - Context, - Block extends ReadonlyArray, - ResultOut extends BlockResultOut, - ParamsOut = GetFirstBlockParams, - BlockResult = GetFirstBlockResult, - BeforeResultOut = unknown, - AfterResultOut = unknown, - ErrorResultOut = unknown, - Params = GetFirstBlockParams, ->({ block, options }: { - block: FirstBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; -}) { - return new FirstBlock({ block, options }); +const first: { + < + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetFirstBlockParams, + BlockResult = GetFirstBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetFirstBlockParams, + >(args: { + block: FirstBlockDefinition; + options: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required: true }; + }): FirstBlock & { readonly __isRequired: true }; + < + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetFirstBlockParams, + BlockResult = GetFirstBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetFirstBlockParams, + >(args: { + block: FirstBlockDefinition; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + }): FirstBlock; +} = function({ block, options }: any): any { + return new FirstBlock({ block, options }); }; -const pipe = function< - Context, - Block extends ReadonlyArray, - ResultOut extends BlockResultOut, - ParamsOut = GetPipeBlockParams, - BlockResult = GetPipeBlockResult, - BeforeResultOut = unknown, - AfterResultOut = unknown, - ErrorResultOut = unknown, - Params = GetPipeBlockParams, ->({ block, options }: { - block: PipeBlockDefinition; - options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; -}) { - return new PipeBlock({ block, options }); +const pipe: { + < + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetPipeBlockParams, + BlockResult = GetPipeBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetPipeBlockParams, + >(args: { + block: PipeBlockDefinition; + options: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required: true }; + }): PipeBlock & { readonly __isRequired: true }; + < + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetPipeBlockParams, + BlockResult = GetPipeBlockResult, + BeforeResultOut = unknown, + AfterResultOut = unknown, + ErrorResultOut = unknown, + Params = GetPipeBlockParams, + >(args: { + block: PipeBlockDefinition; + options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; + }): PipeBlock; +} = function({ block, options }: any): any { + return new PipeBlock({ block, options }); }; const isBlock = function(block: any) { diff --git a/lib/objectBlock.ts b/lib/objectBlock.ts index 5e7500e..f73135e 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< @@ -106,21 +108,42 @@ class ObjectBlock< } extend< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ExtendedResultOut extends - BlockResultOut, - // ExtendedCustomBlock extends ObjectBlockDefinition, + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >(args: { + options: DescriptBlockOptions & { required: true }; + }): ObjectBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >( + this: ObjectBlock & { readonly __isRequired: true }, + args: { + options?: DescriptBlockOptions & { required?: true }; + } + ): ObjectBlock & { readonly __isRequired: true }; + extend< + ExtendedResultOut extends BlockResultOut, ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, - >({ options }: { - options: DescriptBlockOptions< - Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams - >; - }) { + >(args: { + options?: DescriptBlockOptions; + }): ObjectBlock; + extend({ options }: { options?: any }): any { return new ObjectBlock({ block: this.extendBlock(this.block), options: this.extendOptions(this.options, options) as typeof options, diff --git a/lib/pipeBlock.ts b/lib/pipeBlock.ts index e9a607f..9375fcf 100644 --- a/lib/pipeBlock.ts +++ b/lib/pipeBlock.ts @@ -1,11 +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, - InferResultFromBlock, + InferResultOrError, InferParamsInFromBlock, Tail, DescriptBlockOptions, @@ -34,19 +33,12 @@ export type GetPipeBlockParams< type GetPipeBlockResultUnion> = { 0: never; - 1: First | DescriptError; - 2: First | DescriptError | GetPipeBlockResultUnion>; + 1: InferResultOrError>; + 2: InferResultOrError> | GetPipeBlockResultUnion>; }[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; -type GetPipeBlockResultMap> = { - [ P in keyof T ]: InferResultFromBlock; -}; - -export type GetPipeBlockResult< - T extends ReadonlyArray, - PA extends ReadonlyArray = GetPipeBlockResultMap, - PU = GetPipeBlockResultUnion, -> = PU; +export type GetPipeBlockResult> = + GetPipeBlockResultUnion; export type PipeBlockDefinition = { [ P in keyof T ]: T[ P ] extends BaseBlock< @@ -83,21 +75,39 @@ class PipeBlock< > { extend< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // ExtendedResultOut extends - // BlockResultOut, - // ExtendedCustomBlock extends PipeBlockDefinition, ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = unknown, ExtendedAfterResultOut = unknown, ExtendedErrorResultOut = unknown, - >({ options }: { - options: DescriptBlockOptions< - Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams - >; - }) { + >(args: { + options: DescriptBlockOptions & { required: true }; + }): PipeBlock, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams> & { readonly __isRequired: true }; + extend< + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >( + this: PipeBlock & { readonly __isRequired: true }, + args: { + options?: DescriptBlockOptions & { required?: true }; + } + ): PipeBlock, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams> & { readonly __isRequired: true }; + extend< + ExtendedParamsOut extends Params = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = unknown, + ExtendedAfterResultOut = unknown, + ExtendedErrorResultOut = unknown, + >(args: { + options?: DescriptBlockOptions; + }): PipeBlock, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams>; + extend({ options }: { options?: any }): any { return new PipeBlock({ block: this.extendBlock(this.block), options: this.extendOptions(this.options, options) as typeof options, diff --git a/lib/types.ts b/lib/types.ts index 4700502..24f04fb 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -104,6 +104,11 @@ export type InferResultOrResultOnce = Result extends BaseBlock< infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params > ? ResultOut : Result; +export type InferResultOrError = + T extends { readonly __isRequired: true } + ? InferResultFromBlock + : InferResultFromBlock | DescriptError; + 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 73d794f1e712d96dd385f004b67ae64e52a036ad Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Mon, 13 Apr 2026 18:41:44 +0300 Subject: [PATCH 2/5] required fix --- problems/deps-prev.examples.ts | 163 ++++++++++++ problems/params.examples.ts | 447 +++++++++++++++++++++++++++++++++ problems/params.ts | 42 ++++ problems/required.examples.ts | 235 +++++++++++++++++ 4 files changed, 887 insertions(+) create mode 100644 problems/deps-prev.examples.ts create mode 100644 problems/params.examples.ts create mode 100644 problems/params.ts create mode 100644 problems/required.examples.ts diff --git a/problems/deps-prev.examples.ts b/problems/deps-prev.examples.ts new file mode 100644 index 0000000..8df0d7c --- /dev/null +++ b/problems/deps-prev.examples.ts @@ -0,0 +1,163 @@ +/** + * Примеры для проблемы типизации deps.prev в de.pipe + * + * deps.prev — массив результатов предыдущих блоков в пайпе. + * Каждый следующий блок получает его через deps.prev в callbacks (before/after/params/error). + * + * Проблема двухуровневая: + * 1. deps.prev не объявлен в типе DescriptBlockDeps вообще (TS ругается на обращение) + * 2. Даже если обойти это через (deps as any).prev — тип будет any, без проверок + * + * Запуск проверки типов: npx tsc --noEmit + */ + +import * as de from '../lib'; + +// ============================================================================= +// Вспомогательные типы результатов блоков +// ============================================================================= + +interface UserResult { + id: number; + name: string; +} + +interface OrderResult { + orderId: string; + total: number; +} + +// Блок 1: возвращает UserResult +const fetchUser = de.http({ + block: { pathname: '/user' }, + options: { + after: () => ({ id: 1, name: 'Alice' } as UserResult), + }, +}); + +// Блок 2: возвращает OrderResult +const fetchOrder = de.http({ + block: { pathname: '/order' }, + options: { + after: () => ({ orderId: 'ORD-42', total: 100 } as OrderResult), + }, +}); + +// ============================================================================= +// ПРОБЛЕМА 1: deps.prev не существует в типе DescriptBlockDeps +// ============================================================================= +// +// DescriptBlockDeps = Record +// Поле prev устанавливается в рантайме через @ts-ignore (см. block.ts:252-254), +// но в типах оно не объявлено — TypeScript не знает о его существовании. + +const blockA = de.http({ + block: { pathname: '/a' }, + options: { + before: ({ deps }) => { + // [COMPILE ERROR] Property 'prev' does not exist on type 'DescriptBlockDeps' + // TypeScript прав — поле не объявлено. Но оно ЕСТЬ в рантайме! + // + deps.prev; // <-- раскомментировать, чтобы увидеть ошибку + + void deps; + }, + }, +}); + +// ============================================================================= +// ПРОБЛЕМА 2: единственный способ добраться до deps.prev — это (deps as any).prev +// После этого все проверки типов отключаются +// ============================================================================= + +const summarize = de.http({ + block: { pathname: '/summary' }, + options: { + // Этот блок стоит третьим в пайпе: [fetchUser, fetchOrder, summarize] + // deps.prev должен быть [UserResult, OrderResult], но TypeScript об этом не знает + before: ({ deps }) => { + // Разработчик вынужден кастить, чтобы вообще получить доступ к prev + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + const prev = (deps as any).prev as unknown[]; + + // ================================================================= + // После каста — никаких проверок, все ошибки ниже молчат: + // ================================================================= + + // [INVALID] несуществующее поле — нет ошибки + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bad1 = (prev[ 0 ] as any).nonExistentField; + + // [INVALID] перепутан индекс: prev[0] — UserResult, у него нет orderId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bad2 = (prev[ 0 ] as any).orderId; + + // [INVALID] индекс за пределами массива (пайп из 3 блоков, prev.length max = 2) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bad3 = (prev[ 5 ] as any).id; + + // [INVALID] присваиваем OrderResult переменной типа UserResult — нет ошибки + const bad4: UserResult = prev[ 1 ] as UserResult; + + // ================================================================= + // Как должно работать при правильной типизации: + // ================================================================= + // + // prev[0] → тип UserResult ✓ + // prev[0].name → string ✓ + // prev[0].orderId → TS ERROR: не существует на UserResult + // prev[1] → тип OrderResult ✓ + // prev[1].total → number ✓ + // prev[5] → TS ERROR: tuple length is 2 + // const x: UserResult = prev[1] → TS ERROR: OrderResult не совместим + + void bad1; + void bad2; + void bad3; + void bad4; + }, + }, +}); + +// ============================================================================= +// ПРОБЛЕМА 3: блок с deps.prev поставлен не на то место в пайпе +// TypeScript никак не защищает от неправильного порядка +// ============================================================================= + +const blockExpectsUserFirst = de.http({ + block: { pathname: '/check' }, + options: { + before: ({ deps }) => { + // Разработчик ожидает UserResult на позиции 0 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const prev = (deps as any).prev as [ UserResult, OrderResult ]; + const user = prev[ 0 ]; // ожидает UserResult + // undefined в рантайме — блок стоит ПЕРВЫМ, prev === [] + void user; + }, + }, +}); + +// [INVALID] blockExpectsUserFirst стоит первым — deps.prev будет пустым массивом []. +// Должна быть ошибка типа: блок ожидает prev[0] = UserResult, но он первый в пайпе. +// TypeScript не ругается. +const _wrong_pipe = de.pipe({ + block: [ + blockExpectsUserFirst, // prev = [] — краш в рантайме + fetchUser, + fetchOrder, + ] as const, +}); + +// [VALID] правильный порядок +const _correct_pipe = de.pipe({ + block: [ + fetchUser, // prev = [] + fetchOrder, // prev = [UserResult] + summarize, // prev = [UserResult, OrderResult] + ] as const, +}); + +void blockA; +void _wrong_pipe; +void _correct_pipe; diff --git a/problems/params.examples.ts b/problems/params.examples.ts new file mode 100644 index 0000000..5f3bdba --- /dev/null +++ b/problems/params.examples.ts @@ -0,0 +1,447 @@ +/** + * Примеры для проблемы типизации params.ts + * + * Здесь собраны сценарии, которые ДОЛЖНЫ и НЕ ДОЛЖНЫ работать. + * Сейчас TypeScript не ловит ошибочные случаи — это то, что нужно починить. + * + * Запуск проверки типов: npx tsc --noEmit + */ + +import * as de from '../lib'; + +// ============================================================================= +// Вспомогательные типы +// ============================================================================= + +interface ParamsA { + a: string; +} + +interface ParamsB { + b: number; +} + +interface ParamsAB { + a: string; + b: number; +} + +interface ParamsABC { + a: string; + b: number; + c: boolean; +} + +// ============================================================================= +// 1. БАЗОВЫЙ СЛУЧАЙ — возврат одного блока из de.func +// ============================================================================= + +// [VALID] params родителя содержит всё, что нужно дочернему блоку +const _case1_valid = de.func({ + block: ({ params }: { params: ParamsAB }) => { + return de.http({ + block: { + pathname: ({ params }: { params: ParamsA }) => `/resource/${params.a}`, + }, + }); + }, +}); + +// [INVALID] params родителя не содержит то, что нужно дочернему блоку +// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. +const _case1_invalid = de.func({ + block: ({ params }: { params: ParamsA }) => { + // ParamsA = { a: string }, но дочерний блок требует ParamsB = { b: number } + return de.http({ + block: { + pathname: ({ params }: { params: ParamsB }) => `/resource/${params.b}`, + }, + }); + }, +}); + +// ============================================================================= +// 2. МОСТ ЧЕРЕЗ options.params — родитель трансформирует params для дочернего +// ============================================================================= + +// [VALID] options.params преобразует ParamsA → ParamsB +const _case2_valid = de.func({ + block: ({ params }: { params: ParamsA }) => { + return de.http({ + block: { + pathname: ({ params }: { params: ParamsB }) => `/resource/${params.b}`, + }, + options: { + params: ({ params }: { params: ParamsA }): ParamsB => ({ + b: params.a.length, + }), + }, + }); + }, +}); + +// ============================================================================= +// 3. de.object — пересечение params всех дочерних блоков +// ============================================================================= + +const blockNeedsA = de.http({ + block: { + pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}`, + }, +}); + +const blockNeedsB = de.http({ + block: { + pathname: ({ params }: { params: ParamsB }) => `/b/${params.b}`, + }, +}); + +// [VALID] родитель даёт ParamsAB, object требует ParamsA & ParamsB = ParamsAB +const _case3_valid = de.func({ + block: ({ params }: { params: ParamsAB }) => { + return de.object({ + block: { + resA: blockNeedsA, + resB: blockNeedsB, + }, + }); + }, +}); + +// [INVALID] родитель даёт только ParamsA, но object требует ParamsA & ParamsB +// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. +const _case3_invalid = de.func({ + block: ({ params }: { params: ParamsA }) => { + return de.object({ + block: { + resA: blockNeedsA, + resB: blockNeedsB, // требует ParamsB, которых нет в ParamsA + }, + }); + }, +}); + +// ============================================================================= +// 4. de.array +// ============================================================================= + +// [VALID] родитель даёт ParamsAB, array требует ParamsA & ParamsB = ParamsAB +const _case4_valid = de.func({ + block: ({ params }: { params: ParamsAB }) => { + return de.array({ + block: [ blockNeedsA, blockNeedsB ] as const, + }); + }, +}); + +// [INVALID] родитель даёт только ParamsA, array требует ParamsA & ParamsB +// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. +const _case4_invalid = de.func({ + block: ({ params }: { params: ParamsA }) => { + return de.array({ + block: [ blockNeedsA, blockNeedsB ] as const, + }); + }, +}); + +// ============================================================================= +// 5. de.first +// ============================================================================= + +// [VALID] родитель даёт ParamsAB, first требует ParamsA & ParamsB +const _case5_valid = de.func({ + block: ({ params }: { params: ParamsAB }) => { + return de.first({ + block: [ blockNeedsA, blockNeedsB ] as const, + }); + }, +}); + +// [INVALID] родитель даёт только ParamsB, first требует ParamsA & ParamsB +// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. +const _case5_invalid = de.func({ + block: ({ params }: { params: ParamsB }) => { + return de.first({ + block: [ blockNeedsA, blockNeedsB ] as const, + }); + }, +}); + +// ============================================================================= +// 6. de.pipe +// ============================================================================= + +// [VALID] родитель даёт ParamsAB, pipe требует ParamsA & ParamsB +const _case6_valid = de.func({ + block: ({ params }: { params: ParamsAB }) => { + return de.pipe({ + block: [ blockNeedsA, blockNeedsB ] as const, + }); + }, +}); + +// [INVALID] родитель даёт только ParamsA, pipe требует ParamsA & ParamsB +// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. +const _case6_invalid = de.func({ + block: ({ params }: { params: ParamsA }) => { + return de.pipe({ + block: [ blockNeedsA, blockNeedsB ] as const, + }); + }, +}); + +// ============================================================================= +// 7. ВЛОЖЕННЫЕ de.func — каждый уровень проверяется независимо +// ============================================================================= + +// [VALID] каждый уровень передаёт достаточно params следующему +const _case7_valid = de.func({ + block: ({ params }: { params: ParamsABC }) => { + return de.func({ + block: ({ params }: { params: ParamsAB }) => { + return de.http({ + block: { + pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}`, + }, + }); + }, + }); + }, +}); + +// [INVALID] средний уровень получает ParamsA, но возвращает блок требующий ParamsAB +const _case7_invalid = de.func({ + block: ({ params }: { params: ParamsABC }) => { + // внешний func даёт ParamsABC → вложенному func → OK (ParamsABC extends ParamsA) + return de.func({ + block: ({ params }: { params: ParamsA }) => { + // средний func имеет только ParamsA, но возвращает блок требующий ParamsAB + return de.http({ + block: { + pathname: ({ params }: { params: ParamsAB }) => `/ab/${params.a}/${params.b}`, + }, + }); + }, + }); + }, +}); + +// ============================================================================= +// 8. CORNER CASE: блок без params (unknown / never) +// ============================================================================= + +const blockNoParams = de.http({ + block: { + hostname: 'example.com', + pathname: '/static', + }, +}); + +// [VALID] дочерний блок вообще не требует params — любой родитель совместим +const _case8_valid = de.func({ + block: ({ params }: { params: ParamsA }) => { + return blockNoParams; // не требует никаких params + }, +}); + +// ============================================================================= +// 9. CORNER CASE: суперсет params — родитель даёт больше, чем нужно +// ============================================================================= + +// [VALID] родитель даёт ParamsABC, дочерний требует только ParamsA — OK (суперсет) +const _case9_valid = de.func({ + block: ({ params }: { params: ParamsABC }) => { + return de.http({ + block: { + pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}`, + }, + }); + }, +}); + +// ============================================================================= +// 10. CORNER CASE: опциональные поля +// ============================================================================= + +interface ParamsOptional { + required: string; + optional?: number; +} + +interface ParamsOnlyRequired { + required: string; +} + +// [VALID] дочерний блок требует { required, optional? } — родитель с { required } подходит, +// так как optional опционален +const _case10_valid = de.func({ + block: ({ params }: { params: ParamsOnlyRequired }) => { + return de.http({ + block: { + pathname: ({ params }: { params: ParamsOptional }) => `/r/${params.required}`, + }, + }); + }, +}); + +// ============================================================================= +// 11. CORNER CASE: динамический выбор блока (union return type) +// ============================================================================= + +const blockNeedsA_func = de.http({ + block: { pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}` }, +}); +const blockNeedsB_func = de.http({ + block: { pathname: ({ params }: { params: ParamsB }) => `/b/${params.b}` }, +}); + +// [VALID] родитель даёт ParamsAB — совместим с blockNeedsA (ParamsA ⊆ ParamsAB) +const _case11_valid = de.func({ + block: (args: { params: ParamsAB }) => { + void args; + return blockNeedsA_func; + }, +}); + +// [VALID] родитель даёт ParamsAB — совместим с blockNeedsB (ParamsB ⊆ ParamsAB) +void de.func({ + block: (args: { params: ParamsAB }) => { + void args; + return blockNeedsB_func; + }, +}); + +// KNOWN LIMITATION: union return (ternary) с conditional constraint. +// TypeScript инферит BlockResult из одного operand тернарного оператора, +// из-за чего constraint ожидает только один тип в return, а не union. +// Работает только однотипный возврат (выше), не union return. + +// [INVALID] родитель даёт ParamsA — совместим с blockNeedsA, но НЕ с blockNeedsB +// При ternary TypeScript инферит BlockResult как union → constraint проверяет каждую ветку +const _case11_invalid = de.func({ + block: ({ params }: { params: ParamsA }) => { + // DescriptParamsError: blockNeedsB_func требует ParamsB, а ParamsA его не содержит + return params.a === 'special' ? blockNeedsA_func : blockNeedsB_func; + }, +}); + +// ============================================================================= +// 12. CORNER CASE: de.func возвращает не блок, а значение (не BaseBlock) +// ============================================================================= + +// [VALID] func возвращает примитив — params вообще не передаются никуда дальше, +// ограничение не нужно +const _case12_valid = de.func({ + block: ({ params }: { params: ParamsA }) => { + return `result: ${params.a}`; + }, +}); + +// ============================================================================= +// 13. CORNER CASE: de.run — финальная проверка совместимости params +// ============================================================================= + +const blockTopLevel = de.func({ + block: ({ params }: { params: ParamsAB }) => { + return de.http({ + block: { + pathname: ({ params }: { params: ParamsAB }) => `/ab/${params.a}/${params.b}`, + }, + }); + }, +}); + +// [VALID] передаём полные params +de.run(blockTopLevel, { + params: { a: 'hello', b: 42 }, +}); + +// [INVALID] передаём неполные params — TypeScript уже ловит это на уровне de.run +de.run(blockTopLevel, { + params: { a: 'hello' }, +}); + + +// ============================================================================= +// 14. CORNER CASE: de.object с options.params который возвращает меньше чем нужно детям +// ============================================================================= + +// [VALID] options.params возвращает ParamsA & ParamsB — достаточно для обоих детей +const _case14_valid = de.object({ + options: { + params: ({ params }: { params: { source: string } }): ParamsAB => ({ + a: params.source, + b: params.source.length, + }), + }, + block: { + resA: blockNeedsA, + resB: blockNeedsB, + }, +}); + +// [INVALID] options.params возвращает только ParamsA, но resB требует ParamsB +const _case14_invalid = de.object({ + options: { + params: ({ params }: { params: ParamsA }) => { + return params; // возвращает ParamsA, но нужно ParamsA & ParamsB + }, + }, + block: { + resA: blockNeedsA, + resB: blockNeedsB, + }, +}); + +const block1 = de.http({ + block: { + pathname: () => `/resource/`, + }, + options: { + params: ({ params }: { params: { id1: number } }) => { + return params; + }, + } +}) + +const block2 = de.http({ + block: { + pathname: () => `/resource/`, + }, + options: { + params: ({ params }: { params: { id2: number } }) => { + return params; + }, + } +}) + + +const block2Func = de.func({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + block: ({ params }: { params: { id1: number } }) => { + return block2.extend({ + options: { + params: () => { + if (params.id1 > 10) { + return ({ id2: params.id1 }); + } + + return { id2: 1 }; + }, + }, + }); + }, +}); + + +const block2Func = de.func({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + block: ({ params }: { params: { id1: number }}) => { + if (params.id1 === 12345) { + return block2; + } + + return block1; + }, +}); + diff --git a/problems/params.ts b/problems/params.ts new file mode 100644 index 0000000..863a7a8 --- /dev/null +++ b/problems/params.ts @@ -0,0 +1,42 @@ +import * as de from 'descript'; + +interface ParamsTop { + param1: string; + param2: string; +} + +interface ParamsResource1 { + resourceParams: string; +} + +const block1 = de.func({ + block: ({ params }: { params: ParamsTop }) => { + return de.object({ + block: { + resource1: de.func({ + block: ({ params }: { params: ParamsResource1 }) => { + console.log('params'); + console.log(params); + + return 'resource1 result' + }, + }) + } + }); + } +}); + +de.run(block1, { + params: { + param1: 'param1', + param2: 'param2', + }, +}) + .then((result) => { + console.log(result); + }); + +// ParamsResource1 - контракт при котором будет работать блок resource1. +// Однако нет проверки на то, что эти параметры будут доставлены до этого блока (в моем примере не доставлены) + +// Нужно проверять параметры всех parent блоков + их результатов метода options.params, и сравнивать с тем, что нужно для вызова блока. При несоответсвии выдавать ошибку типов \ No newline at end of file diff --git a/problems/required.examples.ts b/problems/required.examples.ts new file mode 100644 index 0000000..0a757f9 --- /dev/null +++ b/problems/required.examples.ts @@ -0,0 +1,235 @@ +/** + * Примеры типизации `required` в descript-блоках + * + * Правила: + * - required: true → поле в GetObjectBlockResult НЕ имеет `| DescriptError` + * - required: false → поле имеет `| DescriptError` (дефолт) + * - .extend() без явного required: false наследует бренд от родителя + * - .extend({ options: { required: false } }) снимает бренд + * + * Запуск проверки типов: npx tsc --noEmit + */ + +import * as de from '../lib'; +import { DescriptError } from '../lib/error'; + +interface UserResult { id: number; name: string; } +interface OrderResult { orderId: string; total: number; } +interface ProfileResult { avatarUrl: string; bio: string; } + +// ============================================================================= +// ГРУППА 1: required: false — union с DescriptError нужен +// ============================================================================= + +const blockOptionalProfile = de.http({ + block: { pathname: '/profile' }, + options: { + required: false, + after: () => ({ avatarUrl: '/img/me.png', bio: 'Dev' } as ProfileResult), + }, +}); + +const pageWithOptional = de.object({ + block: { + user: de.http({ + block: { pathname: '/user' }, + options: { after: () => ({ id: 1, name: 'Alice' } as UserResult) }, + }), + profile: blockOptionalProfile, + }, +}); + +async function renderOptional() { + const result = await de.run(pageWithOptional, { params: {} }); + + // user: UserResult | DescriptError — narrowing обязателен + const user = result.user; + if (user instanceof DescriptError) return ''; + + // profile: ProfileResult | DescriptError — narrowing обязателен + const profile = result.profile; + if (profile instanceof DescriptError) { + return ``; + } + return ``; +} + +void renderOptional; + +// ============================================================================= +// ГРУППА 2: required: true — union с DescriptError отсутствует +// ============================================================================= + +const blockRequiredUser = de.http({ + block: { pathname: '/user' }, + options: { + required: true, + after: () => ({ id: 1, name: 'Alice' } as UserResult), + }, +}); + +const pageRequired = de.object({ + block: { + user: blockRequiredUser, // required: true → UserResult (без DescriptError) + profile: blockOptionalProfile, // required: false → ProfileResult | DescriptError + }, +}); + +async function renderRequired() { + const result = await de.run(pageRequired, { params: {} }); + + // user: UserResult — narrowing НЕ нужен + const name: string = result.user.name; + + // profile: ProfileResult | DescriptError — narrowing нужен + const profile = result.profile; + const avatar = profile instanceof DescriptError ? '/img/default.png' : profile.avatarUrl; + + return ``; +} + +void renderRequired; + +// ============================================================================= +// ГРУППА 3: .extend() наследует бренд — не нужно повторять required: true +// ============================================================================= + +// Базовый блок с required: true +const baseUserBlock = de.http({ + block: { pathname: '/user' }, + options: { required: true }, +}); + +// extend без указания required — бренд __isRequired сохраняется +const userWithAfter = baseUserBlock.extend({ + options: { after: () => ({ id: 1, name: 'Alice' } as UserResult) }, +}); + +// Цепочка extend — бренд сохраняется на каждом шаге +const userWithTimeout = userWithAfter.extend({ + options: { timeout: 5000 }, +}); + +const pageInherited = de.object({ + block: { + user: userWithTimeout, // всё ещё required: true → UserResult (без DescriptError) + profile: blockOptionalProfile, + }, +}); + +async function renderInherited() { + const result = await de.run(pageInherited, { params: {} }); + + // user: UserResult — без narrowing, хотя required: true указан только в базовом блоке + const name: string = result.user.name; + + const profile = result.profile; + const avatar = profile instanceof DescriptError ? '/img/default.png' : profile.avatarUrl; + + return ``; +} + +void renderInherited; + +// ============================================================================= +// ГРУППА 4: .extend({ required: false }) снимает бренд +// ============================================================================= + +const baseRequired = de.http({ + block: { pathname: '/user' }, + options: { required: true, after: () => ({ id: 1, name: 'Alice' } as UserResult) }, +}); + +// Явное required: false — бренд снимается +const madeOptional = baseRequired.extend({ + options: { required: false }, +}); + +const pageDowngraded = de.object({ + block: { user: madeOptional }, +}); + +async function renderDowngraded() { + const result = await de.run(pageDowngraded, { params: {} }); + + // user: UserResult | DescriptError — narrowing нужен (бренд снят) + const user = result.user; + if (user instanceof DescriptError) return ''; + return ``; +} + +void renderDowngraded; + +// ============================================================================= +// ГРУППА 5: смешанный сценарий — часть полей required, часть нет +// ============================================================================= + +const mixedPage = de.object({ + block: { + user: de.http({ + block: { pathname: '/user' }, + options: { required: true, after: () => ({ id: 1, name: 'Alice' } as UserResult) }, + }), + order: de.http({ + block: { pathname: '/order' }, + options: { required: true, after: () => ({ orderId: 'ORD-1', total: 99 } as OrderResult) }, + }), + profile: de.http({ + block: { pathname: '/profile' }, + options: { required: false, after: () => ({ avatarUrl: '/img/me.png', bio: 'Dev' } as ProfileResult) }, + }), + }, +}); + +// Тип result: +// { +// user: UserResult ← только тип результата (required: true) +// order: OrderResult ← только тип результата (required: true) +// profile: ProfileResult | DescriptError ← union сохранён (required: false) +// } + +async function renderMixed() { + const result = await de.run(mixedPage, { params: {} }); + + // Прямой доступ без narrowing для required-полей: + const userName: string = result.user.name; + const orderTotal: number = result.order.total; + + // Narrowing только для optional: + const profile = result.profile; + const avatar = profile instanceof DescriptError ? '/img/default.png' : profile.avatarUrl; + + return ``; +} + +void renderMixed; + +// ============================================================================= +// ГРУППА 6: required: true + error-хук — ручное управление ошибкой +// ============================================================================= +// +// error-хук перехватывает ошибку до compositeBlock, поэтому required: true не +// вызывает cancel родителя — блок вернёт результат error-хука. +// Тип поля: BlockResult | ErrorResultOut (без DescriptError). + +const blockRequiredWithErrorHook = de.http({ + block: { pathname: '/user' }, + options: { + required: true, + after: () => ({ id: 1, name: 'Alice' } as UserResult), + error: (): UserResult => ({ id: 0, name: 'Guest' }), + }, +}); + +const pageWithErrorHook = de.object({ + block: { user: blockRequiredWithErrorHook }, +}); + +async function renderWithErrorHook() { + const result = await de.run(pageWithErrorHook, { params: {} }); + // user: UserResult — без DescriptError (required: true + error-хук возвращает UserResult) + const name: string = result.user.name; + return ``; +} + +void renderWithErrorHook; From d09838ea324ead2670088053138c345740f714c1 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Thu, 16 Apr 2026 17:11:02 +0300 Subject: [PATCH 3/5] rm examples --- problems/deps-prev.examples.ts | 163 ------------ problems/params.examples.ts | 447 --------------------------------- problems/params.ts | 42 ---- problems/required.examples.ts | 235 ----------------- 4 files changed, 887 deletions(-) delete mode 100644 problems/deps-prev.examples.ts delete mode 100644 problems/params.examples.ts delete mode 100644 problems/params.ts delete mode 100644 problems/required.examples.ts diff --git a/problems/deps-prev.examples.ts b/problems/deps-prev.examples.ts deleted file mode 100644 index 8df0d7c..0000000 --- a/problems/deps-prev.examples.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Примеры для проблемы типизации deps.prev в de.pipe - * - * deps.prev — массив результатов предыдущих блоков в пайпе. - * Каждый следующий блок получает его через deps.prev в callbacks (before/after/params/error). - * - * Проблема двухуровневая: - * 1. deps.prev не объявлен в типе DescriptBlockDeps вообще (TS ругается на обращение) - * 2. Даже если обойти это через (deps as any).prev — тип будет any, без проверок - * - * Запуск проверки типов: npx tsc --noEmit - */ - -import * as de from '../lib'; - -// ============================================================================= -// Вспомогательные типы результатов блоков -// ============================================================================= - -interface UserResult { - id: number; - name: string; -} - -interface OrderResult { - orderId: string; - total: number; -} - -// Блок 1: возвращает UserResult -const fetchUser = de.http({ - block: { pathname: '/user' }, - options: { - after: () => ({ id: 1, name: 'Alice' } as UserResult), - }, -}); - -// Блок 2: возвращает OrderResult -const fetchOrder = de.http({ - block: { pathname: '/order' }, - options: { - after: () => ({ orderId: 'ORD-42', total: 100 } as OrderResult), - }, -}); - -// ============================================================================= -// ПРОБЛЕМА 1: deps.prev не существует в типе DescriptBlockDeps -// ============================================================================= -// -// DescriptBlockDeps = Record -// Поле prev устанавливается в рантайме через @ts-ignore (см. block.ts:252-254), -// но в типах оно не объявлено — TypeScript не знает о его существовании. - -const blockA = de.http({ - block: { pathname: '/a' }, - options: { - before: ({ deps }) => { - // [COMPILE ERROR] Property 'prev' does not exist on type 'DescriptBlockDeps' - // TypeScript прав — поле не объявлено. Но оно ЕСТЬ в рантайме! - // - deps.prev; // <-- раскомментировать, чтобы увидеть ошибку - - void deps; - }, - }, -}); - -// ============================================================================= -// ПРОБЛЕМА 2: единственный способ добраться до deps.prev — это (deps as any).prev -// После этого все проверки типов отключаются -// ============================================================================= - -const summarize = de.http({ - block: { pathname: '/summary' }, - options: { - // Этот блок стоит третьим в пайпе: [fetchUser, fetchOrder, summarize] - // deps.prev должен быть [UserResult, OrderResult], но TypeScript об этом не знает - before: ({ deps }) => { - // Разработчик вынужден кастить, чтобы вообще получить доступ к prev - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - const prev = (deps as any).prev as unknown[]; - - // ================================================================= - // После каста — никаких проверок, все ошибки ниже молчат: - // ================================================================= - - // [INVALID] несуществующее поле — нет ошибки - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bad1 = (prev[ 0 ] as any).nonExistentField; - - // [INVALID] перепутан индекс: prev[0] — UserResult, у него нет orderId - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bad2 = (prev[ 0 ] as any).orderId; - - // [INVALID] индекс за пределами массива (пайп из 3 блоков, prev.length max = 2) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bad3 = (prev[ 5 ] as any).id; - - // [INVALID] присваиваем OrderResult переменной типа UserResult — нет ошибки - const bad4: UserResult = prev[ 1 ] as UserResult; - - // ================================================================= - // Как должно работать при правильной типизации: - // ================================================================= - // - // prev[0] → тип UserResult ✓ - // prev[0].name → string ✓ - // prev[0].orderId → TS ERROR: не существует на UserResult - // prev[1] → тип OrderResult ✓ - // prev[1].total → number ✓ - // prev[5] → TS ERROR: tuple length is 2 - // const x: UserResult = prev[1] → TS ERROR: OrderResult не совместим - - void bad1; - void bad2; - void bad3; - void bad4; - }, - }, -}); - -// ============================================================================= -// ПРОБЛЕМА 3: блок с deps.prev поставлен не на то место в пайпе -// TypeScript никак не защищает от неправильного порядка -// ============================================================================= - -const blockExpectsUserFirst = de.http({ - block: { pathname: '/check' }, - options: { - before: ({ deps }) => { - // Разработчик ожидает UserResult на позиции 0 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const prev = (deps as any).prev as [ UserResult, OrderResult ]; - const user = prev[ 0 ]; // ожидает UserResult - // undefined в рантайме — блок стоит ПЕРВЫМ, prev === [] - void user; - }, - }, -}); - -// [INVALID] blockExpectsUserFirst стоит первым — deps.prev будет пустым массивом []. -// Должна быть ошибка типа: блок ожидает prev[0] = UserResult, но он первый в пайпе. -// TypeScript не ругается. -const _wrong_pipe = de.pipe({ - block: [ - blockExpectsUserFirst, // prev = [] — краш в рантайме - fetchUser, - fetchOrder, - ] as const, -}); - -// [VALID] правильный порядок -const _correct_pipe = de.pipe({ - block: [ - fetchUser, // prev = [] - fetchOrder, // prev = [UserResult] - summarize, // prev = [UserResult, OrderResult] - ] as const, -}); - -void blockA; -void _wrong_pipe; -void _correct_pipe; diff --git a/problems/params.examples.ts b/problems/params.examples.ts deleted file mode 100644 index 5f3bdba..0000000 --- a/problems/params.examples.ts +++ /dev/null @@ -1,447 +0,0 @@ -/** - * Примеры для проблемы типизации params.ts - * - * Здесь собраны сценарии, которые ДОЛЖНЫ и НЕ ДОЛЖНЫ работать. - * Сейчас TypeScript не ловит ошибочные случаи — это то, что нужно починить. - * - * Запуск проверки типов: npx tsc --noEmit - */ - -import * as de from '../lib'; - -// ============================================================================= -// Вспомогательные типы -// ============================================================================= - -interface ParamsA { - a: string; -} - -interface ParamsB { - b: number; -} - -interface ParamsAB { - a: string; - b: number; -} - -interface ParamsABC { - a: string; - b: number; - c: boolean; -} - -// ============================================================================= -// 1. БАЗОВЫЙ СЛУЧАЙ — возврат одного блока из de.func -// ============================================================================= - -// [VALID] params родителя содержит всё, что нужно дочернему блоку -const _case1_valid = de.func({ - block: ({ params }: { params: ParamsAB }) => { - return de.http({ - block: { - pathname: ({ params }: { params: ParamsA }) => `/resource/${params.a}`, - }, - }); - }, -}); - -// [INVALID] params родителя не содержит то, что нужно дочернему блоку -// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. -const _case1_invalid = de.func({ - block: ({ params }: { params: ParamsA }) => { - // ParamsA = { a: string }, но дочерний блок требует ParamsB = { b: number } - return de.http({ - block: { - pathname: ({ params }: { params: ParamsB }) => `/resource/${params.b}`, - }, - }); - }, -}); - -// ============================================================================= -// 2. МОСТ ЧЕРЕЗ options.params — родитель трансформирует params для дочернего -// ============================================================================= - -// [VALID] options.params преобразует ParamsA → ParamsB -const _case2_valid = de.func({ - block: ({ params }: { params: ParamsA }) => { - return de.http({ - block: { - pathname: ({ params }: { params: ParamsB }) => `/resource/${params.b}`, - }, - options: { - params: ({ params }: { params: ParamsA }): ParamsB => ({ - b: params.a.length, - }), - }, - }); - }, -}); - -// ============================================================================= -// 3. de.object — пересечение params всех дочерних блоков -// ============================================================================= - -const blockNeedsA = de.http({ - block: { - pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}`, - }, -}); - -const blockNeedsB = de.http({ - block: { - pathname: ({ params }: { params: ParamsB }) => `/b/${params.b}`, - }, -}); - -// [VALID] родитель даёт ParamsAB, object требует ParamsA & ParamsB = ParamsAB -const _case3_valid = de.func({ - block: ({ params }: { params: ParamsAB }) => { - return de.object({ - block: { - resA: blockNeedsA, - resB: blockNeedsB, - }, - }); - }, -}); - -// [INVALID] родитель даёт только ParamsA, но object требует ParamsA & ParamsB -// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. -const _case3_invalid = de.func({ - block: ({ params }: { params: ParamsA }) => { - return de.object({ - block: { - resA: blockNeedsA, - resB: blockNeedsB, // требует ParamsB, которых нет в ParamsA - }, - }); - }, -}); - -// ============================================================================= -// 4. de.array -// ============================================================================= - -// [VALID] родитель даёт ParamsAB, array требует ParamsA & ParamsB = ParamsAB -const _case4_valid = de.func({ - block: ({ params }: { params: ParamsAB }) => { - return de.array({ - block: [ blockNeedsA, blockNeedsB ] as const, - }); - }, -}); - -// [INVALID] родитель даёт только ParamsA, array требует ParamsA & ParamsB -// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. -const _case4_invalid = de.func({ - block: ({ params }: { params: ParamsA }) => { - return de.array({ - block: [ blockNeedsA, blockNeedsB ] as const, - }); - }, -}); - -// ============================================================================= -// 5. de.first -// ============================================================================= - -// [VALID] родитель даёт ParamsAB, first требует ParamsA & ParamsB -const _case5_valid = de.func({ - block: ({ params }: { params: ParamsAB }) => { - return de.first({ - block: [ blockNeedsA, blockNeedsB ] as const, - }); - }, -}); - -// [INVALID] родитель даёт только ParamsB, first требует ParamsA & ParamsB -// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. -const _case5_invalid = de.func({ - block: ({ params }: { params: ParamsB }) => { - return de.first({ - block: [ blockNeedsA, blockNeedsB ] as const, - }); - }, -}); - -// ============================================================================= -// 6. de.pipe -// ============================================================================= - -// [VALID] родитель даёт ParamsAB, pipe требует ParamsA & ParamsB -const _case6_valid = de.func({ - block: ({ params }: { params: ParamsAB }) => { - return de.pipe({ - block: [ blockNeedsA, blockNeedsB ] as const, - }); - }, -}); - -// [INVALID] родитель даёт только ParamsA, pipe требует ParamsA & ParamsB -// Сейчас: TypeScript НЕ ругается. ДОЛЖЕН ругаться. -const _case6_invalid = de.func({ - block: ({ params }: { params: ParamsA }) => { - return de.pipe({ - block: [ blockNeedsA, blockNeedsB ] as const, - }); - }, -}); - -// ============================================================================= -// 7. ВЛОЖЕННЫЕ de.func — каждый уровень проверяется независимо -// ============================================================================= - -// [VALID] каждый уровень передаёт достаточно params следующему -const _case7_valid = de.func({ - block: ({ params }: { params: ParamsABC }) => { - return de.func({ - block: ({ params }: { params: ParamsAB }) => { - return de.http({ - block: { - pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}`, - }, - }); - }, - }); - }, -}); - -// [INVALID] средний уровень получает ParamsA, но возвращает блок требующий ParamsAB -const _case7_invalid = de.func({ - block: ({ params }: { params: ParamsABC }) => { - // внешний func даёт ParamsABC → вложенному func → OK (ParamsABC extends ParamsA) - return de.func({ - block: ({ params }: { params: ParamsA }) => { - // средний func имеет только ParamsA, но возвращает блок требующий ParamsAB - return de.http({ - block: { - pathname: ({ params }: { params: ParamsAB }) => `/ab/${params.a}/${params.b}`, - }, - }); - }, - }); - }, -}); - -// ============================================================================= -// 8. CORNER CASE: блок без params (unknown / never) -// ============================================================================= - -const blockNoParams = de.http({ - block: { - hostname: 'example.com', - pathname: '/static', - }, -}); - -// [VALID] дочерний блок вообще не требует params — любой родитель совместим -const _case8_valid = de.func({ - block: ({ params }: { params: ParamsA }) => { - return blockNoParams; // не требует никаких params - }, -}); - -// ============================================================================= -// 9. CORNER CASE: суперсет params — родитель даёт больше, чем нужно -// ============================================================================= - -// [VALID] родитель даёт ParamsABC, дочерний требует только ParamsA — OK (суперсет) -const _case9_valid = de.func({ - block: ({ params }: { params: ParamsABC }) => { - return de.http({ - block: { - pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}`, - }, - }); - }, -}); - -// ============================================================================= -// 10. CORNER CASE: опциональные поля -// ============================================================================= - -interface ParamsOptional { - required: string; - optional?: number; -} - -interface ParamsOnlyRequired { - required: string; -} - -// [VALID] дочерний блок требует { required, optional? } — родитель с { required } подходит, -// так как optional опционален -const _case10_valid = de.func({ - block: ({ params }: { params: ParamsOnlyRequired }) => { - return de.http({ - block: { - pathname: ({ params }: { params: ParamsOptional }) => `/r/${params.required}`, - }, - }); - }, -}); - -// ============================================================================= -// 11. CORNER CASE: динамический выбор блока (union return type) -// ============================================================================= - -const blockNeedsA_func = de.http({ - block: { pathname: ({ params }: { params: ParamsA }) => `/a/${params.a}` }, -}); -const blockNeedsB_func = de.http({ - block: { pathname: ({ params }: { params: ParamsB }) => `/b/${params.b}` }, -}); - -// [VALID] родитель даёт ParamsAB — совместим с blockNeedsA (ParamsA ⊆ ParamsAB) -const _case11_valid = de.func({ - block: (args: { params: ParamsAB }) => { - void args; - return blockNeedsA_func; - }, -}); - -// [VALID] родитель даёт ParamsAB — совместим с blockNeedsB (ParamsB ⊆ ParamsAB) -void de.func({ - block: (args: { params: ParamsAB }) => { - void args; - return blockNeedsB_func; - }, -}); - -// KNOWN LIMITATION: union return (ternary) с conditional constraint. -// TypeScript инферит BlockResult из одного operand тернарного оператора, -// из-за чего constraint ожидает только один тип в return, а не union. -// Работает только однотипный возврат (выше), не union return. - -// [INVALID] родитель даёт ParamsA — совместим с blockNeedsA, но НЕ с blockNeedsB -// При ternary TypeScript инферит BlockResult как union → constraint проверяет каждую ветку -const _case11_invalid = de.func({ - block: ({ params }: { params: ParamsA }) => { - // DescriptParamsError: blockNeedsB_func требует ParamsB, а ParamsA его не содержит - return params.a === 'special' ? blockNeedsA_func : blockNeedsB_func; - }, -}); - -// ============================================================================= -// 12. CORNER CASE: de.func возвращает не блок, а значение (не BaseBlock) -// ============================================================================= - -// [VALID] func возвращает примитив — params вообще не передаются никуда дальше, -// ограничение не нужно -const _case12_valid = de.func({ - block: ({ params }: { params: ParamsA }) => { - return `result: ${params.a}`; - }, -}); - -// ============================================================================= -// 13. CORNER CASE: de.run — финальная проверка совместимости params -// ============================================================================= - -const blockTopLevel = de.func({ - block: ({ params }: { params: ParamsAB }) => { - return de.http({ - block: { - pathname: ({ params }: { params: ParamsAB }) => `/ab/${params.a}/${params.b}`, - }, - }); - }, -}); - -// [VALID] передаём полные params -de.run(blockTopLevel, { - params: { a: 'hello', b: 42 }, -}); - -// [INVALID] передаём неполные params — TypeScript уже ловит это на уровне de.run -de.run(blockTopLevel, { - params: { a: 'hello' }, -}); - - -// ============================================================================= -// 14. CORNER CASE: de.object с options.params который возвращает меньше чем нужно детям -// ============================================================================= - -// [VALID] options.params возвращает ParamsA & ParamsB — достаточно для обоих детей -const _case14_valid = de.object({ - options: { - params: ({ params }: { params: { source: string } }): ParamsAB => ({ - a: params.source, - b: params.source.length, - }), - }, - block: { - resA: blockNeedsA, - resB: blockNeedsB, - }, -}); - -// [INVALID] options.params возвращает только ParamsA, но resB требует ParamsB -const _case14_invalid = de.object({ - options: { - params: ({ params }: { params: ParamsA }) => { - return params; // возвращает ParamsA, но нужно ParamsA & ParamsB - }, - }, - block: { - resA: blockNeedsA, - resB: blockNeedsB, - }, -}); - -const block1 = de.http({ - block: { - pathname: () => `/resource/`, - }, - options: { - params: ({ params }: { params: { id1: number } }) => { - return params; - }, - } -}) - -const block2 = de.http({ - block: { - pathname: () => `/resource/`, - }, - options: { - params: ({ params }: { params: { id2: number } }) => { - return params; - }, - } -}) - - -const block2Func = de.func({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - block: ({ params }: { params: { id1: number } }) => { - return block2.extend({ - options: { - params: () => { - if (params.id1 > 10) { - return ({ id2: params.id1 }); - } - - return { id2: 1 }; - }, - }, - }); - }, -}); - - -const block2Func = de.func({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - block: ({ params }: { params: { id1: number }}) => { - if (params.id1 === 12345) { - return block2; - } - - return block1; - }, -}); - diff --git a/problems/params.ts b/problems/params.ts deleted file mode 100644 index 863a7a8..0000000 --- a/problems/params.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as de from 'descript'; - -interface ParamsTop { - param1: string; - param2: string; -} - -interface ParamsResource1 { - resourceParams: string; -} - -const block1 = de.func({ - block: ({ params }: { params: ParamsTop }) => { - return de.object({ - block: { - resource1: de.func({ - block: ({ params }: { params: ParamsResource1 }) => { - console.log('params'); - console.log(params); - - return 'resource1 result' - }, - }) - } - }); - } -}); - -de.run(block1, { - params: { - param1: 'param1', - param2: 'param2', - }, -}) - .then((result) => { - console.log(result); - }); - -// ParamsResource1 - контракт при котором будет работать блок resource1. -// Однако нет проверки на то, что эти параметры будут доставлены до этого блока (в моем примере не доставлены) - -// Нужно проверять параметры всех parent блоков + их результатов метода options.params, и сравнивать с тем, что нужно для вызова блока. При несоответсвии выдавать ошибку типов \ No newline at end of file diff --git a/problems/required.examples.ts b/problems/required.examples.ts deleted file mode 100644 index 0a757f9..0000000 --- a/problems/required.examples.ts +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Примеры типизации `required` в descript-блоках - * - * Правила: - * - required: true → поле в GetObjectBlockResult НЕ имеет `| DescriptError` - * - required: false → поле имеет `| DescriptError` (дефолт) - * - .extend() без явного required: false наследует бренд от родителя - * - .extend({ options: { required: false } }) снимает бренд - * - * Запуск проверки типов: npx tsc --noEmit - */ - -import * as de from '../lib'; -import { DescriptError } from '../lib/error'; - -interface UserResult { id: number; name: string; } -interface OrderResult { orderId: string; total: number; } -interface ProfileResult { avatarUrl: string; bio: string; } - -// ============================================================================= -// ГРУППА 1: required: false — union с DescriptError нужен -// ============================================================================= - -const blockOptionalProfile = de.http({ - block: { pathname: '/profile' }, - options: { - required: false, - after: () => ({ avatarUrl: '/img/me.png', bio: 'Dev' } as ProfileResult), - }, -}); - -const pageWithOptional = de.object({ - block: { - user: de.http({ - block: { pathname: '/user' }, - options: { after: () => ({ id: 1, name: 'Alice' } as UserResult) }, - }), - profile: blockOptionalProfile, - }, -}); - -async function renderOptional() { - const result = await de.run(pageWithOptional, { params: {} }); - - // user: UserResult | DescriptError — narrowing обязателен - const user = result.user; - if (user instanceof DescriptError) return ''; - - // profile: ProfileResult | DescriptError — narrowing обязателен - const profile = result.profile; - if (profile instanceof DescriptError) { - return ``; - } - return ``; -} - -void renderOptional; - -// ============================================================================= -// ГРУППА 2: required: true — union с DescriptError отсутствует -// ============================================================================= - -const blockRequiredUser = de.http({ - block: { pathname: '/user' }, - options: { - required: true, - after: () => ({ id: 1, name: 'Alice' } as UserResult), - }, -}); - -const pageRequired = de.object({ - block: { - user: blockRequiredUser, // required: true → UserResult (без DescriptError) - profile: blockOptionalProfile, // required: false → ProfileResult | DescriptError - }, -}); - -async function renderRequired() { - const result = await de.run(pageRequired, { params: {} }); - - // user: UserResult — narrowing НЕ нужен - const name: string = result.user.name; - - // profile: ProfileResult | DescriptError — narrowing нужен - const profile = result.profile; - const avatar = profile instanceof DescriptError ? '/img/default.png' : profile.avatarUrl; - - return ``; -} - -void renderRequired; - -// ============================================================================= -// ГРУППА 3: .extend() наследует бренд — не нужно повторять required: true -// ============================================================================= - -// Базовый блок с required: true -const baseUserBlock = de.http({ - block: { pathname: '/user' }, - options: { required: true }, -}); - -// extend без указания required — бренд __isRequired сохраняется -const userWithAfter = baseUserBlock.extend({ - options: { after: () => ({ id: 1, name: 'Alice' } as UserResult) }, -}); - -// Цепочка extend — бренд сохраняется на каждом шаге -const userWithTimeout = userWithAfter.extend({ - options: { timeout: 5000 }, -}); - -const pageInherited = de.object({ - block: { - user: userWithTimeout, // всё ещё required: true → UserResult (без DescriptError) - profile: blockOptionalProfile, - }, -}); - -async function renderInherited() { - const result = await de.run(pageInherited, { params: {} }); - - // user: UserResult — без narrowing, хотя required: true указан только в базовом блоке - const name: string = result.user.name; - - const profile = result.profile; - const avatar = profile instanceof DescriptError ? '/img/default.png' : profile.avatarUrl; - - return ``; -} - -void renderInherited; - -// ============================================================================= -// ГРУППА 4: .extend({ required: false }) снимает бренд -// ============================================================================= - -const baseRequired = de.http({ - block: { pathname: '/user' }, - options: { required: true, after: () => ({ id: 1, name: 'Alice' } as UserResult) }, -}); - -// Явное required: false — бренд снимается -const madeOptional = baseRequired.extend({ - options: { required: false }, -}); - -const pageDowngraded = de.object({ - block: { user: madeOptional }, -}); - -async function renderDowngraded() { - const result = await de.run(pageDowngraded, { params: {} }); - - // user: UserResult | DescriptError — narrowing нужен (бренд снят) - const user = result.user; - if (user instanceof DescriptError) return ''; - return ``; -} - -void renderDowngraded; - -// ============================================================================= -// ГРУППА 5: смешанный сценарий — часть полей required, часть нет -// ============================================================================= - -const mixedPage = de.object({ - block: { - user: de.http({ - block: { pathname: '/user' }, - options: { required: true, after: () => ({ id: 1, name: 'Alice' } as UserResult) }, - }), - order: de.http({ - block: { pathname: '/order' }, - options: { required: true, after: () => ({ orderId: 'ORD-1', total: 99 } as OrderResult) }, - }), - profile: de.http({ - block: { pathname: '/profile' }, - options: { required: false, after: () => ({ avatarUrl: '/img/me.png', bio: 'Dev' } as ProfileResult) }, - }), - }, -}); - -// Тип result: -// { -// user: UserResult ← только тип результата (required: true) -// order: OrderResult ← только тип результата (required: true) -// profile: ProfileResult | DescriptError ← union сохранён (required: false) -// } - -async function renderMixed() { - const result = await de.run(mixedPage, { params: {} }); - - // Прямой доступ без narrowing для required-полей: - const userName: string = result.user.name; - const orderTotal: number = result.order.total; - - // Narrowing только для optional: - const profile = result.profile; - const avatar = profile instanceof DescriptError ? '/img/default.png' : profile.avatarUrl; - - return ``; -} - -void renderMixed; - -// ============================================================================= -// ГРУППА 6: required: true + error-хук — ручное управление ошибкой -// ============================================================================= -// -// error-хук перехватывает ошибку до compositeBlock, поэтому required: true не -// вызывает cancel родителя — блок вернёт результат error-хука. -// Тип поля: BlockResult | ErrorResultOut (без DescriptError). - -const blockRequiredWithErrorHook = de.http({ - block: { pathname: '/user' }, - options: { - required: true, - after: () => ({ id: 1, name: 'Alice' } as UserResult), - error: (): UserResult => ({ id: 0, name: 'Guest' }), - }, -}); - -const pageWithErrorHook = de.object({ - block: { user: blockRequiredWithErrorHook }, -}); - -async function renderWithErrorHook() { - const result = await de.run(pageWithErrorHook, { params: {} }); - // user: UserResult — без DescriptError (required: true + error-хук возвращает UserResult) - const name: string = result.user.name; - return ``; -} - -void renderWithErrorHook; From 8cd54703d037bb41aa0e8ad62151fc54229e65c5 Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Fri, 17 Apr 2026 14:50:37 +0300 Subject: [PATCH 4/5] fix pipe result type --- lib/pipeBlock.ts | 9 ++------- lib/types.ts | 4 ++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/pipeBlock.ts b/lib/pipeBlock.ts index 9375fcf..cfca164 100644 --- a/lib/pipeBlock.ts +++ b/lib/pipeBlock.ts @@ -6,6 +6,7 @@ import type { First, InferResultOrError, InferParamsInFromBlock, + Last, Tail, DescriptBlockOptions, } from './types'; @@ -31,14 +32,8 @@ export type GetPipeBlockParams< PU = GetPipeBlockParamsUnion, > = PU; -type GetPipeBlockResultUnion> = { - 0: never; - 1: InferResultOrError>; - 2: InferResultOrError> | GetPipeBlockResultUnion>; -}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; - export type GetPipeBlockResult> = - GetPipeBlockResultUnion; + InferResultOrError>; export type PipeBlockDefinition = { [ P in keyof T ]: T[ P ] extends BaseBlock< diff --git a/lib/types.ts b/lib/types.ts index 24f04fb..42eed3b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -166,6 +166,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>; From aaf9b75c35c979783c24e8020a747b71cb9245de Mon Sep 17 00:00:00 2001 From: aleksandrvsl Date: Fri, 17 Apr 2026 15:53:33 +0300 Subject: [PATCH 5/5] DeepResolveResult --- lib/index.ts | 12 +++++++++--- lib/types.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index e9d7108..19107d6 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -24,6 +24,8 @@ import type { DescriptBlockOptions, DescriptHttpBlockHeaders, InferResultFromBlock, + DeepInferResultFromBlock, + DeepResolveResult, InferParamsInFromBlock, InferBlock, InferHttpBlock, @@ -32,7 +34,7 @@ import type { } from './types'; import type BaseBlock from './block'; import type { DescriptHttpBlockDescription, DescriptHttpBlockQuery, DescriptHttpBlockQueryValue } from './httpBlock'; -import type { GetObjectBlockParams, GetObjectBlockResult, ObjectBlockDefinition } from './objectBlock'; +import type { GetObjectBlockParams, GetObjectBlockResult } from './objectBlock'; import type { GetArrayBlockParams, GetArrayBlockResult, ArrayBlockDefinition } from './arrayBlock'; import type { GetFirstBlockParams, GetFirstBlockResult, FirstBlockDefinition } from './firstBlock'; import type { GetPipeBlockParams, GetPipeBlockResult, PipeBlockDefinition } from './pipeBlock'; @@ -103,6 +105,7 @@ const array: { const object: { < Context, + // eslint-disable-next-line @typescript-eslint/no-explicit-any Blocks extends Record, ResultOut extends BlockResultOut, ParamsOut = GetObjectBlockParams, @@ -112,11 +115,12 @@ const object: { ErrorResultOut = unknown, Params = GetObjectBlockParams, >(args: { - block?: ObjectBlockDefinition; + block?: Blocks; options: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params> & { required: true }; }): ObjectBlock & { readonly __isRequired: true }; < Context, + // eslint-disable-next-line @typescript-eslint/no-explicit-any Blocks extends Record, ResultOut extends BlockResultOut, ParamsOut = GetObjectBlockParams, @@ -126,7 +130,7 @@ const object: { ErrorResultOut = unknown, Params = GetObjectBlockParams, >(args?: { - block?: ObjectBlockDefinition; + block?: Blocks; options?: DescriptBlockOptions, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params>; }): ObjectBlock; } = function({ block, options }: any = {}): any { @@ -298,6 +302,8 @@ export { GenerateId, DescriptBlockId, InferResultFromBlock, + DeepInferResultFromBlock, + DeepResolveResult, InferParamsInFromBlock, InferBlock, DescriptBlockDeps, diff --git a/lib/types.ts b/lib/types.ts index 42eed3b..7c92f47 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -116,6 +116,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,