Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tuple inference widening when function literals are included in tuple literals #61228

Open
niieani opened this issue Feb 20, 2025 · 1 comment

Comments

@niieani
Copy link

niieani commented Feb 20, 2025

🔎 Search Terms

  • tuple inference with function literal
  • inference tuple function and object literal
  • mixing tuple objects and function widening

🕗 Version & Regression Information

  • This is the behavior in every version I tried (from 4.0.5 to 5.8.1-rc), and I reviewed the FAQ for entries possibly related to this

⏯ Playground Link

Playground

💻 Code

interface MatcherObj {
  name: string;
}

type MatcherFn = (name: string) => boolean

type Matcher = MatcherObj | MatcherFn

type MapTuple<KeysTuple extends unknown[], MapToValue> = {
  [Index in keyof KeysTuple]: MapToValue;
}

interface Definition<MatchersT extends Matcher[]> {
  matches: [...MatchersT];
  computeValueFromMatches: (
    ...matches: MapTuple<MatchersT, Matcher[]>
  ) => number;
}

interface Config<Defs extends {[K in keyof Defs]: Definition<any>}> {
  definition: Defs;
}

function create<Defs extends {[K in keyof Defs]: Matcher[]}>(
  config: Config<{ [K in keyof Defs]: Definition<Defs[K]> }>
) {
  // ...
}


create({
  definition: {
    valid: {
      matches: [{ name: "m3" }],
      computeValueFromMatches: (m) => m.length,
    },
    too_mang_args: {
      matches: [{ name: "m1" }, { name: "m2" }],
      // below correctly errors "Target signature provides too few arguments. Expected 3 or more, but got 2."
      computeValueFromMatches: (m1, m2, m3) => m1.length + m2.length,
    },
    not_enough_args: {
      matches: [],
      // below correctly errors "Target signature provides too few arguments. Expected 1 or more, but got 0."
      computeValueFromMatches: (m) => 123
    },
    // valid_using_function: {
    //   matches: [(a) => a === 'x'],
    //   computeValueFromMatches: (m) => m.length
    // },

    // as soon as you uncomment 'valid_using_function' all the errors go away, and inference breaks!
  },
})

🙁 Actual behavior

When all elements in the matches tuple are object literals, TypeScript infers an exact tuple type (e.g. [MatcherObj] or [MatcherObj, MatcherObj]), and the signature of computeValueFromMatches enforces the correct number of arguments.

However, if a function literal (or an object with a function as a property) is included in the tuple—as shown in the commented-out valid_using_function case — the inference for ALL the cases breaks (not just the one property!), as the entire type widens to MatcherObj[].

This causes the relationship between the number of tuple elements and the number of parameters expected by computeValueFromMatches to be lost, so TypeScript no longer emits errors for other keys when the number of parameters is incorrect.

🙂 Expected behavior

TypeScript should preserve the literal tuple type even when a function literal is included.
In the example case, the tuple length of the matches property should be inferred exactly as provided, so that computeValueFromMatches enforces a parameter count matching the tuple length.

This behavior should be consistent regardless of whether the tuple elements are object literals or function literals.

Additional information about the issue

This issue does not occur when using object literals exclusively in the matches tuple. It only manifests when a function literal is included, suggesting that the inference algorithm incorrectly widens the tuple type in that scenario.

What's even more bizarre, even if you change the Matcher type to be an object whose a property can be a function, the same issue occurs! i.e.

type Matcher = { name: string | (name: string) => boolean }

As soon as any of the defined matches uses a function under the name property, e.g. { name: () => true }, causes same widening behavior!

I have thoroughly searched the TypeScript repository and online discussions but have not found a related issue.

@Andarist
Copy link
Contributor

This is, at the very least, a duplicate of #56241 . The situation presented here is a little bit more complex so maybe fixing it would require some extra work beyond fixing #56241 but that's hard to tell on the spot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants