Skip to content

Inconsistent Parameter Type Inference for Functions with Default Values #60222

Closed

Description

@dukasn

🔎 Search Terms

"parameter type inference" "default parameters" "function type inference issue" "any type" "inconsistent type inference" "default value"

🕗 Version & Regression Information

  • When did you start seeing this bug occur?

I started observing this behavior in TypeScript version 5.5.4, and it has continued in the latest stable release 5.6.3.

  • Have you tried using the nightly version of TypeScript?

Yes, I have tested the latest nightly version (5.7.0-dev.20241014), and the issue persists.

  • Bisecting the issue using every-ts:

I have not yet bisected the issue with every-ts, but I will consider doing this to identify the specific change.

  • Using the Playground:

I have tested the issue in the TypeScript Playground with versions back to 3.3, and the problem is reproducible across multiple versions.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.3#code/PTAEBUAsFNQMQK4DsDGAXAlgeyQZ1AAZqQa4GgDW0AngO5YBOAJqCjmtAB5oBQa1AB1hwkAYXZc0oALygA3j1BLQuLAgYpoALlAAlaG2YAeXGgYYkAcwA0oAIZJqAPh4BfHjxAQYoAGbJ0bCRQJmhfCwxMHD5BYSQjcBl4MQluWwAFUEloJCZ8B2oAbQBdJIKS210kgDcsDCYnJIAKYlIdcFsAOm6BOwY7AFtcHXSAShlG3Q9+IVAAQUcRJP1DJhMzCxtklxjZgHkBKLwjOCzuHLz5xaRG2QVle0CcXAB+HTg3D39UI9ABBiwln6AwSZw4uXwC2oIicTSwhyCw1AByOuASTnGclA7k8YD26lAEUwdgANvY0BwBodQGgsI8SNBqrB-vDoAwabFCUhfGycpoeCygYMmvdlHYnngdKKHkpfFgsE0BEkAEyY1y2LwASTwHDsLCwvi5PIY5isoAA5EgEAMAEZs822cCxADKKHM1PhaHwcvZ5oK5s681Y6mwCHwKEgdU0tgsoWgTE6ihlSmguAEip0VttbJVao1YAAopwBCSMChIhyBJt7CSSVhaPhWo26fQGBR7PgLODQgmk8mbVgbU0kVm7Qw1aAvFBSKAcLBWf0OPhcAN5cQSdRbAwwiSDJgzcRYFwhOh46A7ZA7NVsOyDRAXW6MIdEw93K5RjwgA

💻 Code

// The Functions `this` keyword context
type FnContext = {
    source: Record<string, any>
}

// The function definition
type Fn<T = FnContext, P extends any[] = any[], R = void> = (this: T, ...params: P) => R

type AnyFn = Record<string, Fn>

type Options<F extends AnyFn> = {
    actions?: F
}

function program<T extends AnyFn>(options: Options<T>) { }

// Our initial attempt to achieve proper type inference
program({
    actions: {
        foo(p = 2) {}, // Instead of inferring 'number', TypeScript opts for 'any'. A curious choice, indeed.
        esp(p: number = 2) {}, // Explicit typing allows this to work as intended.
        bob(s: number) {} // This one operates smoothly, reflecting the expected behavior of TypeScript.
    }
})

🙁 Actual behavior

The foo function is inferred as (method) foo(this: FnContext, p?: any): void, which is unexpected and undesired.
The esp and bob functions behave as expected.

🙂 Expected behavior

  1. The foo function should have its p parameter type inferred as number, due to the default value provided.
  2. The esp function behaves correctly, with p inferred as number.
  3. The bob function should remain unaffected and should correctly infer s as number.

Additional information about the issue

I have attempted various approaches, including changing the default parameter type to an empty tuple (TypeScript Playground) and using conditional types (TypeScript Playground), but none resolved the underlying issue.
The issue seems to stem from how TypeScript handles parameter inference in the context of default values, leading to unexpected any types.

If possible, I would appreciate any guidance on how to achieve the desired inference without resorting to explicit typing for each parameter. Additionally, any insights into whether this behavior is expected or if it may be addressed in future versions would be greatly appreciated.

Activity

  1. jcalz commented on Oct 14, 2024

    @jcalz
    Contributor

    Looks like a duplicate of #59643

  2. RyanCavanaugh commented on Oct 14, 2024

    @RyanCavanaugh
    Member

    There's an interaction here with the contextual type that's a bit subtle. Consider this sequence of calls:

    declare function foo<T extends string | number>(callback: (arg: T) => void): (arg: T) => T;
    const p1 = foo(n => n)("hello"); // OK
    const p2 = foo((n?) => {
        return n
    })("hello"); // OK;
    const p3 = foo((n?) => {
        n = n ?? 2;
        return n
    })("hello"); // OK;
    const p4 = foo((n = 2) => {
        return n
    })("hello"); // OK;

    If p1 is ok, then p2 should be OK, and if p2 is OK then p3 should be OK, and if p3 is OK then p4 should also be OK. But you're basically saying p4 should not be OK, which implies that one of these equivalences is wrong, or that p1 should be rejected (how?)

    For the most part, TS views parameter defaults as an implementation detail of the function, not an intrinsic part of the signature.

  3. jcalz commented on Oct 14, 2024

    @jcalz
    Contributor

    ( Presumably you either want declare function foo<T extends string | number>(callback: (arg?: T) => void): (arg: T) => T; or declare function foo<T extends string | number | undefined>(callback: (arg: T) => void): (arg: T) => T; in that example )

    Sure, but given that function bar(n = 2) { return n } infers n as being of type number, and that const p5 = foo(bar)("hello") is the error p4 would give, there's some kind of interplay between default arguments and the function signature, right? To me this looks like #59643, which is interpreted as a feature request of the form "wouldn't it be nice if default arguments could, I don't know, maybe be consulted when inferring a parameter in the presence of a particularly unhelpful contextual type?"

  4. RyanCavanaugh commented on Oct 14, 2024

    @RyanCavanaugh
    Member

    It's also close to #58977 which basically proposes the same interaction at call sites

  5. dukasn commented on Oct 14, 2024

    @dukasn
    Author

    Thanks, Joe Calzaretta (@jcalz) and Ryan Cavanaugh (@RyanCavanaugh)! I've made a workaround that works perfectly for my needs:

    type AnyActions = Record<string, any>;
    
    // Utility type to infer parameters for functions supporting default parameters and the `this` keyword
    type InferParametersWithContext<F extends Record<any, any>, T extends any = undefined> = {
        // Iterate over each key in the action mapping
        [Key in keyof F]: 
            // Check if the function matches the expected structure
            F[Key] extends (...params: infer A) => infer R ?
            // If so, infer the parameters and return type while specifying `this` context
            (this: T, ...params: A) => R : 
            // Otherwise, fallback to using the original function's parameters and return type
            (this: T, ...params: Parameters<F[Key]>) => ReturnType<F[Key]>
    };
    
    type Options<F extends AnyActions> = {
        actions?: InferParametersWithContext<F, {source: number}>;
    };

    TypeScript Playground Example

    Thanks again! I believe the issue can be closed now.

  6. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions