Reputation: 5467
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
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!
Upvotes: 1