Search code examples
typescripttypesreturnargumentsinference

Incorrect inference when using the return keyword in an identity function, or when using an optional argument


I'm running into very weird behavior when using identity functions. I'm writing a wizard system with schema (attached is a playground link with a very simplified version), and using constrained identity function to get inference.

The problem occurs in one of the properties that cannot be inferred when I use either of these:

  • When the returned value from the identity function is using the return keyword (rather than a single-line return wrapped with parentheses). OR
  • When declaring an optional argument in the identity function. The argument is declared in the type definition of the identity function, and when using Parameters<typeof myFunction> it's inferred correctly both when declaring the argument and when I not.

Both these issues are super weird to me, which means I'm either missing something very fundamental or I have found 2 rare bugs.

This reproduces in all available playground versions (tried down to 3.3.3), and also in 4.8.

Playground link with relevant code

Probably better to check the playground for code examples, but there:

TYPES DECLERATIONS:

type Schema = Record<string, unknown> // modified from original for the sake of the example, if it doesn't make sense

type StepFunction<
  TSchema extends Schema = Schema,
> = (anything: unknown) => {
  readonly schema: TSchema
  readonly toAnswers?: (keys: keyof TSchema) => unknown
}

function step<TSchema extends Schema = Schema>(
    stepVal: StepFunction<TSchema>,
  ): StepFunction<TSchema> {
    return stepVal
  }

EXAMPLES: Notice the returned object of all functions is the same! The differences are in:

  • whether we use the return keyword or not (?!?!)
  • Whether we have the argument for the step function or not. not that if I do Parameters<typeof myStepValue> even when the argument is missing, it's inferred correctly (!)
// WORKS: `keys` is inferred based on the `schema`
// - no argument for `step` function
// - no `return` keyword
const workingExample = step(() => ({
  schema: {
    attribute: 'anything',
  },
  toAnswers: keys => {
    // RESULT: `keys` inferred successfully as `attribute`
    type Test = string extends typeof keys ? never : 'true'
    const test: Test = 'true'
    return { test }
  },
}))
// FAILS: `keys` is not inferred based on the `schema`
// - has argument for `step` function
const nonWorkingA = step(_something => ({
  schema: {
    attribute: 'anything',
  },
  toAnswers: keys => {
    // RESULT: `keys` failed to inferred hence defaults to `string`
    type Test = string extends typeof keys ? never : 'true'
    const test: Test = 'true'
    return { test }
  },
}))
// FAILS: `keys` is not inferred based on the `schema`
// - has `return` keyword rather than a "single-return" return with parentheses
const nonWorkingB = step(() => {
  return {
    schema: {
      attribute: 'anything',
    },
    toAnswers: keys => {
      // RESULT: `keys` failed to inferred hence defaults to `string`
      type Test = string extends typeof keys ? never : 'true'
      const test: Test = 'true'
      return { test }
    },
  }
})

Solution

  • Apparently this is a TypeScript inference issue about context-sensitive expressions (see https://github.com/microsoft/TypeScript/issues/49951 and https://github.com/microsoft/TypeScript/issues/47599).

    However, I did find a temp solution - by wrapping the return value with an identity function, the edge cases work.

    e.g.:

    step(_something => (identityOfStep({
      schema: {
        attribute: 'anything',
      },
      toAnswers: keys => {
        // RESULT: `keys` failed to inferred hence defaults to `string`
        type Test = string extends typeof keys ? never : 'true'
        const test: Test = 'true'
        return { test }
      },
    })))
    

    Example in a playground