Skip to content

Commit

Permalink
fix(core): when createFunnelSteps extends with no option at initial…
Browse files Browse the repository at this point in the history
… the context assigned never type. (#53)

* fix(core): when `createFunnelSteps` extends with no option at initial, the context assigned never type.

* apply changeset
  • Loading branch information
minuukang authored Sep 8, 2024
1 parent c1f57b7 commit d2e642e
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 183 deletions.
9 changes: 9 additions & 0 deletions .changeset/kind-trains-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@use-funnel/core': patch
'@use-funnel/browser': patch
'@use-funnel/next': patch
'@use-funnel/react-navigation-native': patch
'@use-funnel/react-router-dom': patch
---

fix(core): when createFunnelSteps extends with no option at initial the context assigned never type
2 changes: 1 addition & 1 deletion packages/core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FunnelRouterTransitionOption } from './router.js';
import { CompareMergeContext } from './typeUtil.js';

export type AnyContext = Record<string, unknown>;
export type AnyContext = Record<string, any>;
export type AnyStepContextMap = Record<string, AnyContext>;

export interface FunnelState<TName extends string, TContext = never> {
Expand Down
99 changes: 35 additions & 64 deletions packages/core/src/stepBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,69 @@
export type FunnelStepOption<TContext, TMeta = never> = { meta?: TMeta } & (
| { guard: (data: unknown) => data is TContext }
| { parse: (data: unknown) => TContext }
);
export type FunnelStepGuardOption<TContext> = {
guard: (data: unknown) => data is TContext;
};

export type FunnelStepParseOption<TContext> = {
parse: (data: unknown) => TContext;
};

export type FunnelStepOption<TContext> = FunnelStepGuardOption<TContext> | FunnelStepParseOption<TContext>;

export function funnelStepOptionIsGuard<TContext>(
option: FunnelStepOption<TContext, any>,
): option is { guard: (data: unknown) => data is TContext } {
option: FunnelStepOption<TContext>,
): option is FunnelStepGuardOption<TContext> {
return 'guard' in option && typeof option.guard === 'function';
}

export function funnelStepOptionIsParse<TContext>(
option: FunnelStepOption<TContext, any>,
): option is { parse: (data: unknown) => TContext } {
option: FunnelStepOption<TContext>,
): option is FunnelStepParseOption<TContext> {
return 'parse' in option && typeof option.parse === 'function';
}

type FunnelStepMap = Record<string, FunnelStepOption<any, any> | undefined>;
type FunnelStepMap = Record<string, FunnelStepOption<any> | undefined>;

class FunnelStepBuilder<
class SimpleFunnelStepBuilder<
TContext,
TMeta,
TStepMap extends FunnelStepMap = {},
TPrevFunnelStepOption extends FunnelStepOption<any, any> = never,
TPrevFunnelStepOption extends FunnelStepGuardOption<TContext> = FunnelStepGuardOption<TContext>,
> {
private stepMap: FunnelStepMap = {};
private prevFunnelStepOption: FunnelStepOption<any, any> | undefined;
private prevFunnelStepOption: FunnelStepGuardOption<any> | undefined;

constructor(private meta?: TMeta) {}
extends<TName extends string>(
steps: TName | TName[],
): FunnelStepBuilder<
): SimpleFunnelStepBuilder<
TContext,
TMeta,
TStepMap & {
[K in TName]: TPrevFunnelStepOption;
},
TPrevFunnelStepOption
>;
extends<
TName extends string,
TFunnelStepOption extends FunnelStepOption<any, any>,
TNewContext extends TFunnelStepOption extends FunnelStepOption<infer TContext, any> ? TContext : never,
TNewMeta extends TFunnelStepOption extends FunnelStepOption<any, infer TMeta> ? TMeta : never,
>(
steps: TName | TName[],
option: TFunnelStepOption | ((meta: TMeta) => TFunnelStepOption),
): FunnelStepBuilder<
TNewContext extends unknown ? TContext : TNewContext,
TNewMeta extends unknown ? TMeta : TNewMeta,
TStepMap & {
[K in TName]: TFunnelStepOption;
},
TFunnelStepOption
>;

extends<TName extends string, TRequiredKeys extends keyof TContext>(
steps: TName | TName[],
options: { requiredKeys: TRequiredKeys[] | TRequiredKeys },
): FunnelStepBuilder<
): SimpleFunnelStepBuilder<
Omit<TContext, TRequiredKeys> & Pick<Required<TContext>, TRequiredKeys>,
TMeta,
TStepMap & {
[K in TName]: FunnelStepOption<Omit<TContext, TRequiredKeys> & Pick<Required<TContext>, TRequiredKeys>, TMeta>;
[K in TName]: FunnelStepGuardOption<Omit<TContext, TRequiredKeys> & Pick<Required<TContext>, TRequiredKeys>>;
},
FunnelStepOption<Omit<TContext, TRequiredKeys> & Pick<Required<TContext>, TRequiredKeys>, TMeta>
FunnelStepGuardOption<Omit<TContext, TRequiredKeys> & Pick<Required<TContext>, TRequiredKeys>>
>;
extends(steps: string | string[], option?: unknown) {
let funnelStep: FunnelStepOption<any, any> | undefined;
if (isRequiredKeys(option)) {
const requiredKeys = [
...(isRequiredKeys(this.meta)
? Array.isArray(this.meta.requiredKeys)
? this.meta.requiredKeys
: [this.meta.requiredKeys]
: []),
...(isRequiredKeys(option)
? Array.isArray(option.requiredKeys)
? option.requiredKeys
: [option.requiredKeys]
: []),
];

extends(steps: string | string[], option?: { requiredKeys: string[] | string }) {
let funnelStep: FunnelStepOption<any> | undefined;
if (option != null) {
const requiredKeys = Array.isArray(option.requiredKeys) ? option.requiredKeys : [option.requiredKeys];
const prevFunnelStepOption = this.prevFunnelStepOption;
funnelStep = {
meta: { requiredKeys },
guard: (data): data is never => {
if (typeof data !== 'object' || data == null) {
return false;
}
if (prevFunnelStepOption != null && !prevFunnelStepOption.guard(data)) {
return false;
}
for (const key of requiredKeys) {
if (!(key in data)) {
return false;
Expand All @@ -93,29 +72,21 @@ class FunnelStepBuilder<
return true;
},
};
} else if (option != null) {
funnelStep = typeof option === 'function' ? option(this.meta!) : option;
} else {
funnelStep = this.prevFunnelStepOption;
}
for (const step of Array.isArray(steps) ? steps : [steps]) {
this.stepMap[step] = funnelStep;
}
if (funnelStep?.meta != null) {
this.meta = funnelStep.meta as never;
}
this.prevFunnelStepOption = funnelStep;
return this as never;
}

build(): TStepMap {
return this.stepMap as never;
return this.stepMap as TStepMap;
}
}

function isRequiredKeys(meta: unknown): meta is { requiredKeys: string[] | string } {
return typeof meta === 'object' && meta != null && 'requiredKeys' in meta;
}

export function createFunnelSteps<TContext, TMeta = never>(meta?: TMeta) {
return new FunnelStepBuilder<TContext, TMeta>(meta);
export function createFunnelSteps<TContext>() {
return new SimpleFunnelStepBuilder<TContext>();
}
28 changes: 14 additions & 14 deletions packages/core/src/useFunnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface UseFunnelOptions<TStepContextMap extends AnyStepContextMap> {
id: string;
initial: FunnelStateByContextMap<TStepContextMap>;
steps?: {
[TStepName in keyof TStepContextMap]: FunnelStepOption<TStepContextMap[TStepName], any>;
[TStepName in keyof TStepContextMap]: FunnelStepOption<TStepContextMap[TStepName]>;
};
}

Expand All @@ -34,8 +34,8 @@ export interface UseFunnel<TRouteOption extends RouteOption> {
TStepKeys extends keyof _TStepContextMap = keyof _TStepContextMap,
TStepContext extends _TStepContextMap[TStepKeys] = _TStepContextMap[TStepKeys],
TStepContextMap extends string extends keyof _TStepContextMap
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
>(
options: UseFunnelOptions<TStepContextMap>,
): UseFunnelResults<TStepContextMap, TRouteOption>;
Expand All @@ -49,8 +49,8 @@ export function createUseFunnel<TRouteOption extends RouteOption>(
TStepKeys extends keyof _TStepContextMap = keyof _TStepContextMap,
TStepContext extends _TStepContextMap[TStepKeys] = _TStepContextMap[TStepKeys],
TStepContextMap extends string extends keyof _TStepContextMap
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
>(options: UseFunnelOptions<TStepContextMap>): UseFunnelResults<TStepContextMap, TRouteOption> {
const optionsRef = useUpdatableRef(options);
const router = useFunnelRouter({
Expand Down Expand Up @@ -85,16 +85,16 @@ export function createUseFunnel<TRouteOption extends RouteOption>(
typeof assignContext === 'function'
? assignContext(currentStateRef.current.context)
: {
...currentStateRef.current.context,
...assignContext,
};
...currentStateRef.current.context,
...assignContext,
};
const context = parseStepContext(step, newContext);
return context == null
? optionsRef.current.initial
: ({
step,
context,
} as FunnelStateByContextMap<TStepContextMap>);
step,
context,
} as FunnelStateByContextMap<TStepContextMap>);
};
return {
push: async (...args) => {
Expand All @@ -120,9 +120,9 @@ export function createUseFunnel<TRouteOption extends RouteOption>(
...(validContext == null
? optionsRef.current.initial
: {
step: currentState.step,
context: validContext,
}),
step: currentState.step,
context: validContext,
}),
history,
index: router.currentIndex,
historySteps: router.history as FunnelStateByContextMap<TStepContextMap>[],
Expand Down
9 changes: 8 additions & 1 deletion packages/core/test/memoryRouter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useMemo, useState } from 'react';
import { FunnelRouter } from '../src/router.js';
import { createUseFunnel } from '../src/useFunnel.js';

export const MemoryRouter: FunnelRouter = ({ initialState }) => {
interface MemoryRouterOption {
__foo?: 'bar';
}

export const MemoryRouter: FunnelRouter<MemoryRouterOption> = ({ initialState }) => {
const [history, setHistory] = useState<(typeof initialState)[]>([initialState]);
const [currentIndex, setCurrentIndex] = useState(0);
return useMemo(
Expand All @@ -25,3 +30,5 @@ export const MemoryRouter: FunnelRouter = ({ initialState }) => {
[history, currentIndex],
);
};

export const useFunnel = createUseFunnel<MemoryRouterOption>(MemoryRouter);
Loading

0 comments on commit d2e642e

Please sign in to comment.