admin管理员组

文章数量:1391964

The issue happens when using the function use with one of its arguments of type MiddleWare<Record<string, unknown>>, so I am expecting it to infer custom entries with key of type string and some inferred value of some type. But in reality I get compile error when trying to use the use function

For example when trying to pass MiddleWare<{a: number}, {x: number}, TestEvents> to the function which takes arguments of type MiddleWare<Record<string, unknown>, {x: number}>> I get the error related to one of the MiddleWare interface callbacks process:

Types of property process are incompatible.

Type '(ctx: Context<{ a: number; }, TestEvents>) => { x: number; } | Promise<{ x: number; }>' is not assignable to type '(ctx: Context<Record<string, unknown>, TestEvents>) => { x: number; } | Promise<{ x: number; }>'.

Types of parameters ctx and ctx are incompatible.
Type Context<Record<string, unknown>, TestEvents> is not assignable to type Context<{ a: number; }, TestEvents>

Property 'a' is missing in type Context<Record<string, unknown>, TestEvents> but required in type { a: number; }

The related code looks like this:

export type MiddleWare<
    TInput extends Record<string, unknown>,
    TOutput extends Record<string, unknown>,
    TEvents extends EventMap,
> = {
    name: string
    deps?: (keyof TInput & string)[]
    provides: keyof TOutput & string
    process: (ctx: Context<TInput, TEvents>) => Promise<TOutput> | TOutput
    rollback?: (ctx: Context<TInput & TOutput, TEvents>) => Promise<void> | void
}

And the method which takes the MiddleWare argument

export const createPipeline = <
    TEvents extends EventMap = {},
    TInitial extends Record<string, unknown> = Record<string, unknown>,
>(): Pipeline<
    TInitial,
    Record<string, unknown>,
    TEvents
> => {
    const middlewares: MiddleWare<
        Record<string, unknown>,
        Record<string, unknown>,
        TEvents
    >[] = []
    const eventSystem = createEventSystem<TEvents>()

    return {
        async execute(input) {
            let ctx = createInitialContext(input, eventSystem)
            const hookSystem = createHookSystem<typeof ctx, TEvents>()

            try {
                await hookSystem.trigger('before', ctx)

                for (const mw of middlewares) {
                    // Type assertion for middleware compatibility
                    ctx = await processMiddleware(
                        mw as unknown as MiddleWare<typeof ctx, any, TEvents>,
                        ctx,
                    )
                    Object.freeze(ctx)
                }

                await hookSystem.trigger('after', ctx)
                return ctx as Context<any, TEvents>
            } catch (error) {
                const errorCtx: Context<Record<string, unknown>, TEvents> = {
                    ...ctx,
                    $error: error as Error,
                    $hooks: createHookSystem<
                        Record<string, unknown>,
                        TEvents
                    >(),
                }

                await hookSystem.trigger(
                    'error',
                    errorCtx as Context<any, TEvents>,
                )
                throw errorCtx
            }
        },

        use<TNew extends Record<string, unknown>>(
            mw: MiddleWare<Record<string, unknown>, TNew, TEvents>,
        ) {
            middlewares.push(mw as MiddleWare<any, any, TEvents>)
            return this as unknown as Pipeline<
                TInitial, // try to tinker unknown / never
                Record<string, unknown> & TNew,
                TEvents
            >
        },

        events: eventSystem,
        hooks: createHookSystem<Record<string, unknown>, TEvents>(),
    }
}

Here is how the usage looks like:

// First middleware: adds property 'x'
        // no issues no errors, context infers and looks fine, no any values etc
        const mw1: MiddleWare<{ 'a': number }, { 'x': number }, TestEvents> = {
            name: 'addX',
            provides: 'x',
            process: (ctx) => ({ ...ctx, 'x': ctx['a'] + 10 }),
        }

        // Second middleware: uses 'x' to produce property 'y'
        // same situation here
        const mw2: MiddleWare<{ a: number; x: number }, { y: number }, TestEvents> = {
            name: 'addY',
            deps: ['x'],
            provides: 'y',
            process: (ctx) => ({ ...ctx, y: ctx.x * 2 }),
        }

        const pipeline = createPipeline<TestEvents>()

        // Use both middlewares in the pipeline.
        // ! TS2345 !
        pipeline
            .use(mw1) // <{a: number},{x: number},TestEvents> not assignable to <Record<string, unknown>,...>
            .use(mw2)

        // Execute the pipeline.
        const input = { a: 5 }
        const result = await pipeline.execute(input)

I tried to tinker never and unknown types in use and execute functions to instantiate empty value at first and then assign new types to it, but it didn't work

The issue happens when using the function use with one of its arguments of type MiddleWare<Record<string, unknown>>, so I am expecting it to infer custom entries with key of type string and some inferred value of some type. But in reality I get compile error when trying to use the use function

For example when trying to pass MiddleWare<{a: number}, {x: number}, TestEvents> to the function which takes arguments of type MiddleWare<Record<string, unknown>, {x: number}>> I get the error related to one of the MiddleWare interface callbacks process:

Types of property process are incompatible.

Type '(ctx: Context<{ a: number; }, TestEvents>) => { x: number; } | Promise<{ x: number; }>' is not assignable to type '(ctx: Context<Record<string, unknown>, TestEvents>) => { x: number; } | Promise<{ x: number; }>'.

Types of parameters ctx and ctx are incompatible.
Type Context<Record<string, unknown>, TestEvents> is not assignable to type Context<{ a: number; }, TestEvents>

Property 'a' is missing in type Context<Record<string, unknown>, TestEvents> but required in type { a: number; }

The related code looks like this:

export type MiddleWare<
    TInput extends Record<string, unknown>,
    TOutput extends Record<string, unknown>,
    TEvents extends EventMap,
> = {
    name: string
    deps?: (keyof TInput & string)[]
    provides: keyof TOutput & string
    process: (ctx: Context<TInput, TEvents>) => Promise<TOutput> | TOutput
    rollback?: (ctx: Context<TInput & TOutput, TEvents>) => Promise<void> | void
}

And the method which takes the MiddleWare argument

export const createPipeline = <
    TEvents extends EventMap = {},
    TInitial extends Record<string, unknown> = Record<string, unknown>,
>(): Pipeline<
    TInitial,
    Record<string, unknown>,
    TEvents
> => {
    const middlewares: MiddleWare<
        Record<string, unknown>,
        Record<string, unknown>,
        TEvents
    >[] = []
    const eventSystem = createEventSystem<TEvents>()

    return {
        async execute(input) {
            let ctx = createInitialContext(input, eventSystem)
            const hookSystem = createHookSystem<typeof ctx, TEvents>()

            try {
                await hookSystem.trigger('before', ctx)

                for (const mw of middlewares) {
                    // Type assertion for middleware compatibility
                    ctx = await processMiddleware(
                        mw as unknown as MiddleWare<typeof ctx, any, TEvents>,
                        ctx,
                    )
                    Object.freeze(ctx)
                }

                await hookSystem.trigger('after', ctx)
                return ctx as Context<any, TEvents>
            } catch (error) {
                const errorCtx: Context<Record<string, unknown>, TEvents> = {
                    ...ctx,
                    $error: error as Error,
                    $hooks: createHookSystem<
                        Record<string, unknown>,
                        TEvents
                    >(),
                }

                await hookSystem.trigger(
                    'error',
                    errorCtx as Context<any, TEvents>,
                )
                throw errorCtx
            }
        },

        use<TNew extends Record<string, unknown>>(
            mw: MiddleWare<Record<string, unknown>, TNew, TEvents>,
        ) {
            middlewares.push(mw as MiddleWare<any, any, TEvents>)
            return this as unknown as Pipeline<
                TInitial, // try to tinker unknown / never
                Record<string, unknown> & TNew,
                TEvents
            >
        },

        events: eventSystem,
        hooks: createHookSystem<Record<string, unknown>, TEvents>(),
    }
}

Here is how the usage looks like:

// First middleware: adds property 'x'
        // no issues no errors, context infers and looks fine, no any values etc
        const mw1: MiddleWare<{ 'a': number }, { 'x': number }, TestEvents> = {
            name: 'addX',
            provides: 'x',
            process: (ctx) => ({ ...ctx, 'x': ctx['a'] + 10 }),
        }

        // Second middleware: uses 'x' to produce property 'y'
        // same situation here
        const mw2: MiddleWare<{ a: number; x: number }, { y: number }, TestEvents> = {
            name: 'addY',
            deps: ['x'],
            provides: 'y',
            process: (ctx) => ({ ...ctx, y: ctx.x * 2 }),
        }

        const pipeline = createPipeline<TestEvents>()

        // Use both middlewares in the pipeline.
        // ! TS2345 !
        pipeline
            .use(mw1) // <{a: number},{x: number},TestEvents> not assignable to <Record<string, unknown>,...>
            .use(mw2)

        // Execute the pipeline.
        const input = { a: 5 }
        const result = await pipeline.execute(input)

I tried to tinker never and unknown types in use and execute functions to instantiate empty value at first and then assign new types to it, but it didn't work

Share Improve this question edited Mar 14 at 9:06 jonrsharpe 122k30 gold badges268 silver badges476 bronze badges asked Mar 14 at 9:00 weuoimiweuoimi 233 bronze badges 1
  • i suggest to recreate the problem on typescriptlang./play . otherwise it's hard to debug it given number of undefined types/functions – Alexander Nenashev Commented Mar 14 at 9:08
Add a comment  | 

1 Answer 1

Reset to default 0

solved it by adding a helper function to create middleware

type InferInput<TProcess> = TProcess extends (ctx: Context<infer I, any>) => any
    ? I
    : never

type InferOutput<TProcess> = TProcess extends (
    ctx: Context<any, any>,
) => infer O
    ? O
    : never

export function defineMiddleware<
    TProcess extends (ctx: any) => any,
    TEvents extends Record<string, (...args: any[]) => void> = {},
>(mw: {
    name: string
    deps?: (keyof InferInput<TProcess> & string)[]
    provides: keyof InferOutput<TProcess> & string
    process: TProcess
    rollback?: (
        ctx: Context<InferInput<TProcess> & InferOutput<TProcess>, TEvents>,
    ) => void | Promise<void>
}): MiddleWare<InferInput<TProcess>, InferOutput<TProcess>, TEvents> {
    return mw as MiddleWare<
        InferInput<TProcess>,
        InferOutput<TProcess>,
        TEvents
    >
}

本文标签: