question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Contextual type can't be provided to a mapped type intersected with an object type

See original GitHub issue

Bug Report

🔎 Search Terms

intersection, mapped type, contextual type

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ

⏯ Playground Link

Playground

💻 Code

type Action<TEvent extends { type: string }> = (ev: TEvent) => void;

interface MachineConfig<TEvent extends { type: string }> {
  schema: {
    events: TEvent;
  };
  on?: {
    [K in TEvent["type"]]?: Action<TEvent extends { type: K } ? TEvent : never>;
  } & {
    "*"?: Action<TEvent>;
  };
}

declare function createMachine<TEvent extends { type: string }>(
  config: MachineConfig<TEvent>
): void;

createMachine({
  schema: {
    events: {} as { type: "FOO" } | { type: "BAR" },
  },
  on: {
    FOO: (ev) => {
      ev.type; // should be 'FOO', but `ev` is typed implicitly as `any`
    },
  },
});

🙁 Actual behavior

An implicit any pop-ups when the contextual type could be, somewhat easily, provided.

🙂 Expected behavior

This should just work 😜 I know a workaround for this issue - the workaround is to use a single mapped type instead of an intersection and just “dispatch” to the correct value in the template~ part of the mapped type, like here. However, this is way less ergonomic than an intersection AND the mapped type is no longer homomorphic which could matter for some cases (well, the original mapped type here is not homomorphic either, but it could be)

I already have a draft PR open to fix this issue, here. I only need some help with the stuff mentioned in the comment here

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:16 (7 by maintainers)

github_iconTop GitHub Comments

2reactions
phryneascommented, Aug 13, 2022

Lemme try an explanation 😄

We’re letting the user provide an object in the form

{
  reducer(state, action: PayloadAction<something>) {},
  prepare(arg) { return actionPayload } 
}

now, we want the return type of actionPayload to be the same as something up in PayloadAction. But we cannot just add a generic <T> somewhere to make sure that’s the case - because this object is not the only object being passed in, but one of many objects inside a config object:

{
  reducers: {
    foo: { reducer: ... ,  prepare: ... },
    bar: { reducer: ... ,  prepare: ... },
  }
}

Now, TS has no syntax to allow for different ActionPayload types for foo and bar while having those internally consistent (foo only has FooActionPayload both on reducer and prepare and bar only has BarActionPayload both on reducer and prepare).

So what we do is that we let TypeScript infer this whole configuration object including all reducers (the “first pass”) and then, when we have that, we use that ConfigObject to restrict it against itself (the “second pass”) - ConfigObject extends Validated<ConfigObject> where Validated is a generic that infers FooActionPayload from ReturnType<ConfigObject['reducers']['foo']['prepare']> and makes sure that the second argument to reducer matches that type.

It’s amazing that we could do something like that in the first place, but it’s also pretty necessary here to make the api work in a type-safe manner.

1reaction
phryneascommented, Aug 16, 2022

Good point, here is a test case: Playground link

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

{
  createSlice({
    name: "test",
    initialState: 0,
    reducers: {
      // normal reducer
      test(state, action: PayloadAction<number>) {
        return state + action.payload
      }
    }
  })
}


{
  createSlice({
    name: "test",
    initialState: 0,
    reducers: {
      // reducer with prepare
      test: {
        reducer(state, action: PayloadAction<number>) {
          return state + action.payload
        },
        prepare(arg: number) {
          return { payload: arg * 2 }
        }
      }
    }
  })
}


{
  createSlice({
    name: "test",
    initialState: 0,
    reducers: {
      // reducer with incorrect prepare
      test: {
        // @ts-expect-error action payload needs to be a number, as returned by `prepare`
        reducer(state, action: PayloadAction<string>) {
          return state + action.payload
        },
        prepare(arg: number) {
          return { payload: arg * 2 }
        }
      }
    }
  })
}


{
  createSlice({
    name: "test",
    initialState: 0,
    reducers: {
      // normal reducer
      test1(state, action: PayloadAction<number>) {
        return state + action.payload
      },
      // reducer with prepare
      test2: {
        reducer(state, action: PayloadAction<number>) {
          return state + action.payload
        },
        prepare(arg: number) {
          return { payload: arg * 2 }
        }
      },
      // reducer with incorrect prepare
      test3: {
        // @ts-expect-error action payload needs to be a number, as returned by `prepare`
        reducer(state, action: PayloadAction<string>) {
          return state + action.payload
        },
        prepare(arg: number) {
          return { payload: arg * 2 }
        }
      },
      // another reducer with incorrect prepare and different payload type
      test4: {
        // @ts-expect-error action payload needs to be { value: number }, as returned by `prepare`
        reducer(state, action: PayloadAction<{value: string}>) {
          return state + action.payload.value
        },
        prepare(arg: number) {
          return { payload: { value: arg * 2 }}
        }
      }
    }
  })
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Mastering TypeScript mapped types - LogRocket Blog
In this post, we'll cover mapped types in TypeScript, a real-world example of them, and utility types including Partial, Readonly, and Pick.
Read more >
Creating a mapped type from an object without errors in ...
How do you create a fully mapped type, given the inability to create a mapped object without intermediate results in JavaScript?
Read more >
Documentation - Type Inference - TypeScript
In TypeScript, there are several places where type inference is used to provide type information when there is no explicit type annotation.
Read more >
Mapped Types - OpenAPI - A progressive Node.js framework
Hint The OmitType() function is imported from the @nestjs/swagger package. Intersection#. The IntersectionType() function combines two types into one new type ( ...
Read more >
Advanced Types
Table of contents #. Intersection Types. Union Types. Type Guards and Differentiating Types. User-Defined Type Guards. Using type predicates; Using the in ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found