-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Type narrowing of generic types in control flow analysis #50652
Comments
const stateReducer = <Type extends keyof State>(
state: State,
- action: { type: Type; value: string } | { type: 'clear'; value: number },
+ action: Type extends unknown ? { type: Type; value: string } | { type: 'clear'; value: number } : never,
) => { |
It’s a fair question why - action: { type: Type; value: string } | { type: 'clear'; value: number },
+ action: { type: keyof State; value: string } | { type: 'clear'; value: number }, The fact that |
It's funny you say this because this was also my understanding for a long time. It's only due to past comments by @RyanCavanaugh that I found out the rule is the discriminant can either be a unit type or a union of unit types (thus why It's interesting that that conditional type trick works. I would expect it to just defer since it's distributive over a type parameter. |
Here is another toy example demonstrating what I think is the same issue: interface UpdateReport<B extends boolean = true> {
newValue: string;
oldValue: B extends true ? string : undefined;
}
export const processReport = function<B extends boolean>(
report: UpdateReport<B>,
hasOldValue: readonly [B],
) {
if(hasOldValue[0]) {
//Error TS(2345): Argument of type 'UpdateReport<B>'
//is not assignable to parameter of type 'UpdateReport<true>'.
//Type 'B' is not assignable to type 'true'.
//Type 'boolean' is not assignable to type 'true'.
//However, within this conditional check / type guard,
//TS should be able to figure out that B is 'true'.
let oldValue = getOldValueFromReport(report); //...
}
}
const getOldValueFromReport = function(
report: UpdateReport<true>
) {
return report.oldValue;
}; Search terms: Type guard narrowing assignable to parameter of type 2345 |
Another one when using unions with generics. type BcryptConfig = {
rounds: number
type: 'bcrypt'
}
type ArgonConfig = {
variant: number
type: 'argon'
}
function defineConfig<T extends { [key: string]: ArgonConfig | BcryptConfig }>(config: T): T {
return config
}
defineConfig({
passwords: {
type: 'argon',
variant: 1,
rounds: 1,
}
}) I expect the |
any update on this? I find myself FREQUENTLY running into this issue, especially when dealing with things like events where there's multiple different types with different payloads. here's an example: type EventType = 'NEW_MESSAGE' | 'NEW_USER'
type AppEvent =
| { type: 'NEW_MESSAGE'; message: string }
| { type: 'NEW_USER'; user: string }
function fetchEvents<Type extends EventType>(
eventType: Type
): Extract<AppEvent, { type: Type }> {
if (eventType === 'NEW_MESSAGE') {
// this errors, because typescript can't figure out that Type must be 'NEW_MESSAGE'
return {
type: 'NEW_MESSAGE',
message: 'hello, world',
}
} else if (eventType === 'NEW_USER') {
// this errors, because typescript can't figure out that Type must be 'NEW_USER'
return {
type: 'NEW_USER',
user: 'rayzr',
}
} else {
throw new Error(`Unknown event type: ${eventType}`)
}
} I do this kinda thing all the time when making abstractions to handle multiple types of input/output, and it's a pain to have to typecast the return types |
This looks like the same thing except the generic is constrained to something other than a union... but it should still probably be discriminable: type Foo<T extends {}> =
{ a: undefined, b: string } | { a: T, b: number }
function foo<T extends {}>(f: Foo<T>) {
if (f.a == null) {
f.b // should be string, is actually string | number
}
} |
I think this is a slightly different example of the control flow not narrowing the generic, only it's not a property access: declare function takesString(value: string): void
const data = { a: 'foo', b: 1 }
type Data = typeof data
function getValue_generic<K extends keyof Data>(key: K) {
if (key === 'a') {
key // K extends "a" | "b"
const value = data[key] // { a: string; b: number }[K]
takesString(value) // Argument of type 'string | number' is not assignable to parameter of type 'string'.
}
}
function getValue_non_generic(key: keyof Data) {
if (key === 'a') {
key // "a"
const value = data[key] // string
takesString(value) // ok
}
} |
And another toy example (I believe): const foo = {
bar: ['hello', (value: 'hello' | 'bye') => {}],
baz: [3, (value: 3 | 5) => {}],
} as const;
function doWithFoo<T extends keyof typeof foo>(key: T) {
if (key === 'bar') {
const [value, setValue] = foo[key];
setValue('bye'); // error here
}
} https://stackblitz.com/edit/typescript-repro-eqknuz?file=src%2Fmain.ts |
"Type narrowing from control flow analysis doesn't backpropagate to another, type-dependent, conditional parameter's type". The demonstration code below is identical* for both TypeScript and Flowtype.
The code in question: type OneOfTwo = String | Number;
type ConditionalTybe<T, TRUETYBE, FALSETYBE> = T extends String ? TRUETYBE : FALSETYBE;
function funktion<T extends OneOfTwo> (arg1: T, arg2: ConditionalTybe<T, true, false> ) {
if (typeof arg1 == "string") {
if (arg2 === true) {}
if (arg2 === false) {} // why is there no error here?
}
}
funktion('str', true);
funktion('str', false); // correctly shows an error here.
funktion(1, true); // correctly shows an error here.
funktion(1, false); If needed, I can provide a real and useful, and not terribly complex code example of this, where a correctly narrowed type would avoid a superfluous null check (to narrow the possibility of a null value out of a variable). |
Bug Report
Hello,
🔎 Search Terms
type narrowing, generic types, control flow analysis
🕗 Version & Regression Information
(see Playground) When trying to narrow generic types (unions), the control flow analysis is not aware of the narrowing. I also see that this PR: #43183 was supposed to address this.
Please keep and fill in the line that best applies:
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
action.value is "string | number"
🙂 Expected behavior
but should be "number"
Thanks in advance!
The text was updated successfully, but these errors were encountered: