From 648af8150e721f4abaf247df6ed3f83bd2d37cba Mon Sep 17 00:00:00 2001 From: Mikhail Kireev <29187880+kireevmp@users.noreply.github.com> Date: Wed, 17 Jan 2024 01:08:22 +0100 Subject: [PATCH] feat(condition): simplify types --- src/condition/index.ts | 142 +++++++++++++++++++------------------- test-typings/condition.ts | 131 +++++++++++++++++++++++++++++++++-- 2 files changed, 198 insertions(+), 75 deletions(-) diff --git a/src/condition/index.ts b/src/condition/index.ts index f49a7f28..7c836cef 100644 --- a/src/condition/index.ts +++ b/src/condition/index.ts @@ -7,91 +7,91 @@ import { Store, UnitTargetable, split, + UnitValue, EventCallable, + EventCallableAsReturnType, } from 'effector'; -type NoInfer = T & { [K in keyof T]: T[K] }; -type EventAsReturnType = any extends Payload ? Event : never; +type NoInfer = [T][T extends any ? 0 : never]; +type NonFalsy = T extends null | undefined | false | 0 | 0n | '' ? never : T; -export function condition(options: { - source: Event; - if: ((payload: State) => boolean) | Store | State; - then: UnitTargetable | void>; - else: UnitTargetable | void>; -}): EventAsReturnType; -export function condition(options: { - source: Store; - if: ((payload: State) => boolean) | Store | State; - then: UnitTargetable; - else: UnitTargetable; -}): Store; -export function condition(options: { - source: Effect; - if: ((payload: Params) => boolean) | Store | Params; - then: UnitTargetable | void>; - else: UnitTargetable | void>; -}): Effect; +type SourceUnit = Store | Event | Effect; -export function condition(options: { - source: Event; - if: ((payload: State) => boolean) | Store | State; - then: UnitTargetable | void>; -}): EventAsReturnType; -export function condition(options: { - source: Store; - if: ((payload: State) => boolean) | Store | State; - then: UnitTargetable | void>; -}): Store; -export function condition(options: { - source: Effect; - if: ((payload: Params) => boolean) | Store | Params; - then: UnitTargetable | void>; -}): Effect; +// -- Without `source`, with type guard -- +export function condition(options: { + source?: undefined; + if: ((payload: Payload) => payload is Then) | Then; + then?: UnitTargetable | void>; + else?: UnitTargetable, Then> | void>; +}): EventCallableAsReturnType; -export function condition(options: { - source: Event; - if: ((payload: State) => boolean) | Store | State; - else: UnitTargetable | void>; -}): EventAsReturnType; -export function condition(options: { - source: Store; - if: ((payload: State) => boolean) | Store | State; - else: UnitTargetable | void>; -}): Store; -export function condition(options: { - source: Effect; - if: ((payload: Params) => boolean) | Store | Params; - else: UnitTargetable | void>; -}): Effect; +// -- Without `source`, with BooleanConstructor -- +export function condition< + Payload, + Then extends NonFalsy = NonFalsy, +>(options: { + source?: undefined; + if: BooleanConstructor; + then?: UnitTargetable | void>; + else?: UnitTargetable, Then> | void>; +}): EventCallableAsReturnType; -// Without `source` +// -- Without `source` -- +export function condition(options: { + source?: undefined; + if: ((payload: Payload) => boolean) | Store | NoInfer; + then?: UnitTargetable | void>; + else?: UnitTargetable | void>; +}): EventCallableAsReturnType; -export function condition(options: { - if: ((payload: State) => boolean) | Store | State; - then: UnitTargetable | void>; - else: UnitTargetable | void>; -}): EventCallable; -export function condition(options: { - if: ((payload: State) => boolean) | Store | State; - then: UnitTargetable | void>; -}): EventCallable; -export function condition(options: { - if: ((payload: State) => boolean) | Store | State; - else: UnitTargetable | void>; -}): EventCallable; -export function condition({ +// -- With `source` and type guard -- +export function condition< + Payload extends UnitValue, + Then extends Payload = Payload, + Source extends SourceUnit = SourceUnit, +>(options: { + source: Source; + if: ((payload: Payload) => payload is Then) | Then; + then?: UnitTargetable>; + else?: UnitTargetable, Then>>; +}): Source; + +// -- With `source` and BooleanConstructor -- +export function condition< + Payload extends UnitValue, + Then extends NonFalsy = NonFalsy, + Source extends SourceUnit = SourceUnit, +>(options: { + source: Source; + if: BooleanConstructor; + then?: UnitTargetable | void>; + else?: UnitTargetable, Then>>; +}): EventCallable; + +// -- With `source` -- +export function condition< + Payload extends UnitValue, + Source extends SourceUnit = SourceUnit, +>(options: { + source: SourceUnit; + if: ((payload: Payload) => boolean) | Store | NoInfer; + then?: UnitTargetable | void>; + else?: UnitTargetable | void>; +}): Source; + +export function condition({ + source = createEvent(), if: test, then: thenBranch, else: elseBranch, - source = createEvent(), }: { - if: ((payload: State) => boolean) | Store | State; - source?: Store | Event | Effect; - then?: UnitTargetable; - else?: UnitTargetable; + source?: SourceUnit; + if: ((payload: Payload) => boolean) | Store | Payload; + then?: UnitTargetable; + else?: UnitTargetable; }) { const checker = - is.unit(test) || isFunction(test) ? test : (value: State) => value === test; + is.unit(test) || isFunction(test) ? test : (value: Payload) => value === test; if (thenBranch && elseBranch) { split({ diff --git a/test-typings/condition.ts b/test-typings/condition.ts index f0c81ae2..6cc8c2de 100644 --- a/test-typings/condition.ts +++ b/test-typings/condition.ts @@ -6,6 +6,7 @@ import { createStore, Effect, Event, + EventCallable, Store, } from 'effector'; import { condition } from '../dist/condition'; @@ -220,25 +221,25 @@ import { condition } from '../dist/condition'; ); } -// Disallow pass invalid type to then/else +// Disallow pass invalid type to then/else/if { condition({ - // @ts-expect-error source: createStore(0), if: 0, + // @ts-expect-error 'string' is not assignable to 'number | void' then: createEvent(), }); condition({ - // @ts-expect-error source: createStore(false), + // @ts-expect-error 'Console' is not assignable to `if` if: console, then: createEvent(), }); condition({ - // @ts-expect-error source: createStore(''), + // @ts-expect-error 'number' is not assignable to type 'boolean' if: (a) => 1, then: createEvent(), }); @@ -259,3 +260,125 @@ import { condition } from '../dist/condition'; else: fxOtherVoid, }); } + +// allows nesting conditions +{ + condition({ + source: createEvent<'a' | 'b' | 1>(), + if: (value): value is 'a' | 'b' => typeof value === 'string', + then: condition<'a' | 'b'>({ + if: () => true, + then: createEvent<'a'>(), + else: createEvent<'b'>(), + }), + }); +} + +// returns `typeof source` when source is provided +{ + const source = createEvent(); + + expectType( + condition({ + source, + if: 'string?', + then: createEvent(), + }), + ); +} + +// Correctly passes type to `if` +{ + condition({ + source: createEvent(), + if: (payload) => (expectType(payload), true), + then: createEvent(), + }); + + condition({ + source: createEvent<'complex' | 'type'>(), + if: (payload) => (expectType<'complex' | 'type'>(payload), true), + then: createEvent(), + }); + + condition({ + source: createEvent(), + // @ts-expect-error 'string' is not assignable to type 'number' + if: (_: number) => true, + then: createEvent(), + }); +} + +// `Boolean` as type guard: disallows invalid type in `then` +{ + condition({ + source: createEvent(), + if: Boolean, + // @ts-expect-error 'number' is not assignable to type 'string | void' + then: createEvent(), + }); +} + +// `Boolean` as type guard: disallows invalid type in then/else +{ + condition({ + source: createEvent(), + if: Boolean, + // @ts-expect-error 'number' is not assignable to type 'string | void' + then: createEvent(), + }); + + condition({ + source: createEvent(), + if: Boolean, + // @ts-expect-error 'number' is not assignable to type 'string | void' + else: createEvent(), + }); +} + +// `Boolean` as type guard: works for all sources +{ + expectType>( + condition({ + source: createEvent(), + if: Boolean, + then: createEvent(), + else: createEvent(), + }), + ); + + expectType>( + condition({ + source: createStore(null), + if: Boolean, + then: createEvent(), + else: createEvent(), + }), + ); + + expectType>( + condition({ + source: createEffect(), + if: Boolean, + then: createEvent(), + else: createEvent(), + }), + ); +} + +// `Boolean` as type guard: disallows invalid type in then/else +{ + condition({ + source: createEvent(), + if: Boolean, + // @ts-expect-error + then: createEvent(), + }); + + condition({ + source: createEvent(), + if: Boolean, + // @ts-expect-error + else: createEvent(), + }); +}