Skip to content

Type narrowing incorrect when providing some function generics when default generics presentΒ #63259

@ChromeQ

Description

@ChromeQ

πŸ”Ž Search Terms

function generic type narrowing
generic inference

πŸ•— Version & Regression Information

This is the behaviour in every version I tried in the playground back to 3.3

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.9.3#code/PTAEGUBUEECVNJAmgBQKLgFABcCeAHAU1FkO1AF5QBDAO1wG4cDiA5agW2KoHIAxAPYCeoAD6geAIWoAnHkzxFQ0GQHMAzgB4EhAB7ZCtACbrQ7LgD5KiUHoPHT-IT0yhQAflABvV278AzIQAuUHVsGQBLWlUmP1AAX18QnX1DEwlpOV83Tx84uIAjWRCwyOjY-MS4kNpCADdCGSZMI0IAYwAbWWJ-AFdaNuwIgVpQf1oAdQjsAAsAeVqAEUJ-al6O7E1fWFtUhxIyABpfVl37dPNuM05CY4sACl9aG5DWY7c62QjqAo7CdRCKg0mlYFmOAEoQrBmq1Ot0xv1BsNRuMprNIAB3ATLVbrTbbM5pUykchUEnvMyE-aXayXO6PNzPLivCmfSI-P4A5RqLSgiFQpggUBoViLRCoDCYTBtEZhUAjQgAcUMjQibWsqOm8yWKzWG00AEYAEwAZgeTmEh282VARRkIR4dp4VqF6hmAnWRnltA6uBoHQ6AgxoAABl4xsFQuEoqoEiHbb1yNQA0HTGGIwIStHonHQHMdum7VmIjIc-F4-nQ+HApmo2UYrbinWY3GbUKaw6ay54uDmjLaHLsFjlbVIuqqJrZgtCDi9ZtjSarRaeObBJbrYUm47ZCJXe7Pd7ff7A8H0x3m2X43QvRFTDKZDJ2tgjxxZABrQhe6imKL3x+DUAokQLAez7WVyFoAQR1VccxkmLVMWxXU8XuZcrTyPwiwkJ1QD3D0Oi9EYj2TE8qwzLN61za9ALvAQHyfF930-GgfwGOj-3IIDIBA3spSAA

πŸ’» Code

// START TYPES
type Ret = any;
type Name = 'Foo' | 'Bar';
type Args<T extends Name> = T extends 'Foo'
  ? {
      foo: string;
    }
  : T extends 'Bar'
    ? {
        bar: string;
      }
    : never;

declare function fnWithOneDefault<
  R extends Ret,
  N extends Name = Name,
>(
  name: N,
  variables: Args<N>,
): R;

declare function fnWithTwoDefault<
  R extends Ret = Ret,
  N extends Name = Name,
>(
  name: N,
  variables: Args<N>,
): R;
// END TYPES

const oneGeneric = fnWithOneDefault<123>('Foo', {
    bar: 'bar', // should only allow `{ foo: string }` but allows `{ foo: string }` OR `{ bar: string }` OR `{ foo: string; bar: string }`
    // foo: 'foo'
});

const twoGeneric = fnWithOneDefault<123, 'Foo'>('Foo', {
    bar: 'bar' // should only allow `{ foo: string }` and is correctly marked as incorrect in TS
});

const noGeneric = fnWithTwoDefault('Foo', {
    bar: 'bar' // should only allow `{ foo: string }` and is correctly marked as incorrect in TS
});

πŸ™ Actual behavior

As per the comments in the code, the oneGeneric function call allows me to provide the args from the 'Bar' name type, or the union of both.
I understand this is due to the 2nd generic N extends Name = Name and when the 2nd generic is omitted then it uses the union of 'Foo' and 'Bar' so both args types are applicable.

ChatGPT revealed:

The key behavior is:

  1. You explicitly provide the first generic (R).
  2. When a generic is explicitly provided, omitted later generics use their defaults rather than being inferred from arguments.
  3. So N becomes its default Name (the union), not 'Foo'.
  4. Args therefore becomes a union of both variable shapes, so the object for Bar is accepted.

Is this "When a generic is explicitly provided, omitted later generics use their defaults rather than being inferred from arguments" intentional or documented?

πŸ™‚ Expected behavior

I would expect the union type of N to be narrowed to 'Foo' when the argument of name is provided as 'Foo' even when the 2nd generic is omitted. It works as expected when both generics have defaults and are both omitted from the calling function as can be seen in the noGeneric function call.

Additional information about the issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions