Skip to content

Control flow analysis for dependant parameters does not work with complex types #51693

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

Open
andyearnshaw opened this issue Nov 30, 2022 · 4 comments
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@andyearnshaw
Copy link

Bug Report

πŸ”Ž Search Terms

dependant parameters
discriminated union parameters

πŸ•— Version & Regression Information

TypeScript 4.6.x -> TypeScript 4.9.3 & Nightly

  • This is the behavior in every version I tried, and I reviewed the FAQ

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

The release notes for 4.6 explain how control flow analysis for dependant parameters can work for functions whose arguments are defined as tuples:

type Func = (...args: ["a", number] | ["b", string]) => void;

const f1: Func = (kind, payload) => {
    if (kind === "a") {
        payload.toFixed();  // 'payload' narrowed to 'number'
    }
    if (kind === "b") {
        payload.toUpperCase();  // 'payload' narrowed to 'string'
    }
};

f1("a", 42);
f1("b", "hello");

However, as soon as you use a more complex type, the code no longer compiles:

type A = { kind: "a"; }
type B = { kind: "b"; }

type Func = (...args: [A, number] | [B, string]) => void;

const f1: Func = ({ kind }, payload) => {
    if (kind === "a") {
        payload.toFixed();  // 'payload' should be narrowed to 'number'
    }
    if (kind === "b") {
        payload.toUpperCase();  // 'payload' should be narrowed to 'string'
    }
};

f1({ kind: "a" }, 42);
f1({ kind: "b" }, "hello");

πŸ™ Actual behavior

On the line with payload.toFixed(), TypeScript gives the following compiler error:

Property 'toFixed' does not exist on type 'string | number'.
  Property 'toFixed' does not exist on type 'string'.

On the line with payload.toUpperCase(), TypeScript gives the following compiler error:

Property 'toUpperCase' does not exist on type 'string | number'.
  Property 'toUpperCase' does not exist on type 'number'.(2339)

πŸ™‚ Expected behavior

I would expect TypeScript to apply the same control flow analysis for the more complex typed parameters as the simple ones. I'm actually trying to use this feature while playing around with the new decorators that @rbuckton has been working on. I want to make a property decorator work with both accessors and setters, taking advantage of control flow analysis to return the appropriate value without casting.

@MartinJohns
Copy link
Contributor

Possibly a duplicate of #46680.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Help Wanted You can do this Experience Enhancement Noncontroversial enhancements labels Dec 2, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Dec 2, 2022
@jsejcksn
Copy link

Possibly a duplicate of #46680.

This appears to be reproducible without destructured rest parameters. Here's another example from this Stack Overflow question:

Playground

interface A {
  type: 'a';
  value: number;
}

interface B {
  type: 'b';
  value: string;
}

function example <T extends A | B>(arg: T, val: T['value']) {
  if (arg.type === 'a') {
    arg.value;
      //^? (property) A.value: number

    val; // should be `number` at this point
  //^? (parameter) val: T["value"]

    val.toFixed(); /*
        ~~~~~~~
    Property 'toFixed' does not exist on type 'string | number'.
      Property 'toFixed' does not exist on type 'string'.(2339) */
  }
}

@exoRift
Copy link

exoRift commented Jan 11, 2023

Yeah, this is an issue with destructuring as well

type typed = {
    type: 'type1',
    prop: string
} | {
    type: 'type2',
    prop: number
}

function returnTyped(param: 'type1' | 'type2'): typed {
    const core = {
        type: param
    }

    switch (core.type) {
        case 'type1':
            return { // destructuring not narrowed
                ...core,
                prop: 'test'
            }
        case 'type2':
            return {
                ...core,
                type: core.type, // bandaid fix is to manually override type with an explicit reference
                prop: 5
            }
    }
}

Playground

It seems to be that variables can get narrowed but not properties (as in properties propagating back up to their parent object)

@Cipscis
Copy link

Cipscis commented Nov 10, 2023

I've believe run into this same issue when trying to work with destructuring tuple unions as function arguments, when one member of the tuple is itself a function:

type Args = [type: 'a', callback: (value: 'a') => void];

declare const testFn: (...[type, value]: Args) => void;

testFn('a', (arg) => {}); // arg is `'a'`

//

type ArgsUnion = [type: 'b', callback: (value: 'b') => void] | [type: 'c', callback: (value: 'c') => void];

declare const testFn2: (...[type, value]: ArgsUnion) => void;

testFn2('b', (arg) => {}); // arg is `any`
testFn2('c', (arg) => {}); // arg is `any`

TypeScript Playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants