Max Gordon
Max Gordon

Reputation: 5467

Typescript conditional property failing to for function argument

The conditional type should allow for smart properties but somehow everything below fails on the last line:

interface Props<T, S extends boolean = boolean> {
  value: T;
  isString: S;
  submit: S extends true ? (arg: string) => void : (arg: T) => void;
}

interface FalseProps<T> {
  value: T;
  isString: false;
  submit: (arg: T) => void;
}

interface TrueProps<T> {
  value: T;
  isString: true;
  submit: (arg: string) => void;
}

function fancyFunction<T>(props: Props<T>): void;
function fancyFunction<T>(props: FalseProps<T> | TrueProps<T>): void {
  if (props.isString === true) {
    props.submit('submit a string');
  } else if (props.isString === false) {
    props.submit(props.value);
  }
}

const args = {
  value: 2,
  isString: true,
  submit: (arg: string) => console.log(arg),
};

fancyFunction(args);

What I get is:

Argument of type '{ value: number; isString: boolean; submit: (arg: string) => void; }' is not assignable to parameter of type 'Props<string, boolean>'.
  Types of property 'value' are incompatible.
    Type 'number' is not assignable to type 'string'.

I'm using version 3.5.2

Note: After receiving @jcalz answer I expanded the question with a follow-up question that can be found here: Typescript conditional property not detecting bad functional argument

Upvotes: 1

Views: 188

Answers (1)

jcalz
jcalz

Reputation: 330481

I think there are a bunch of issues going on here.

First of all your conditional type is being completely eagerly resolved because fancyFunction() is not generic in S, so S takes its default value of boolean, and therefore submit is just of type ((arg: string) => void) | ((arg: T) => void) and Props<T> is no longer a conditional type at all. That's okay, I think, but it might not behave the way you expect it to.

From the point of view of fancyFunction()'s call signature

function fancyFunction<T>(props: Props<T>): void;

, the Props<T> type is

interface Props<T> {
  value: T;
  isString: boolean;
  submit:  ((arg: string) => void) | ((arg: T) => void);
}

Your args value is inferred to be of type

const args: {
    value: number;
    isString: boolean;
    submit: (arg: string) => void;
}

(Aside: were you expecting that isString would be of type true? TypeScript doesn't work that way by default. You can use a const assertion if you want args to be inferred more narrowly)

And you are calling fancyFunction(args). That means that the compiler needs to infer the type T in Props<T> given args. You can see in Props<T> that there are two places T shows up. These are both potential inference sites or T. Meaning, when the compiler looks at args, it might look at both the value property and at the argument type of the submit property to try to figure out T.

And when it looks at the argument of the submit method, it sees string, and thus string becomes a candidate for T. And it tries that and fails. Blecch.

You might have been assuming that the compiler would just use value to infer T, and that the type T in submit should be a "non-inferential type parameter" (maybe you didn't think in those specific terms). Well don't actually have those in TypeScript, but there is an "official" way to lower the priority of an inference site so that it tends not to be consulted as much. You change T to T & {} in the place where you want the inference not to happen:

interface Props<T> {
  value: T;
  isString: boolean;
  submit:  ((arg: string) => void) | ((arg: T & {}) => void); // change here
}

If I make that change, the error goes away:

fancyFunction(args); // okay, T inferred as number

Which is what you want, I think.


Again, you might want to pay more attention to what you're doing with the conditional type and the type of the args variable, just to make sure that you're doing what you intend to do. But the & {} trick should at least help with your generic type inference woes.

Okay, hope that helps. Good luck!

Link to code

Upvotes: 1

Related Questions