microsoft / microsoft/TypeScript

`keyof T['prop']` becomes never when the object contains a context-sensitive function

Open
#55,039 5 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Bug Domain: Indexed Access Types Help Wanted
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

Bug Report

🔎 Search Terms

Not sure how to put this but:

  • recursive type
  • function
  • parameter
  • generics
🕗 Version & Regression Information

Tested on these versions:

  • typescript@5.1.3
  • typescript@5.2.0-dev.20230716 dev
⏯ Playground Link

Playground Link

Code
type Type<
  TAcceptedKeys extends string,
  TType extends Type<TAcceptedKeys>=any
> = {
  /**
   * We're using the keys of this property, that is why
   * we use a recursive type.
   */
  obj?: Record<TAcceptedKeys, 1>
  /**
   * Just for the sake of illustration. 
   * In the circumstances of the bug this will be never[].
   */
  keys: (keyof TType['obj'])[]
  /**
   * A function with a parameter inside the recursive type.
   * This triggers the bug.
   */
  triggerFn?: (param: number) => void
  /**
   * This will always be TType, regardless of whether
   * a parameter is declared in `triggerFn` or not.
   */
  shouldBeTType?: TType
  /**
   * This will be `keyof TType` as expected if a parameter
   * is not declared in triggerFn, but it will turn to `undefined`
   * as soon as a parameter is declared in that function.
   *
   * If the below property is passed to defineType
   * the type will be as expected:
   * triggerFn: () => {}
   *
   * However this will trigger the bug:
   * triggerFn: (param) => {}
   */
  shouldBeKeyofTType?: keyof TType
}

const defineType = <TAcceptedKeys extends string>() =>
  <const TType extends Type<TAcceptedKeys, TType>>(type: TType) => type

defineType<'apple' | 'banana'>()({
  obj: {
    apple: 1,
    banana: 1
  },
  keys: [
    'apple'
  ],

  /**
   * Toggle these comments to see how the bug works.
   */
  triggerFn: () => {},      // won't produce diagnostics
  // triggerFn: (param) => {}, // type 'string' is not assignable to type 'never'

  /**
   * Toggle these comments to see diagnostics information.
   */
  // shouldBeTType: null,
  // shouldBeKeyofTType: null
})
🙁 Actual behavior

The presence or absence of a parameter in an object member function has effect on this object type in certain circumstances.

🙂 Expected behavior

It shouldn't.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked TypeScript Playground and reproduce the recursive Type<TAcceptedKeys, TType> example. Compare the inferred types and diagnostics when triggerFn has no parameter versus a parameter, especially for keys and shouldBeKeyofTType. Done means the function parameter no longer changes the resulting keyof types unexpectedly.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.