-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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
[bug] Type Exclusion Should be Consistent #57511
Comments
related: #55095 |
Another person throwing yet more evidence on the pile that TypeScript is either 1) a gateway drug to type theory or 2) just tends to attract mathematically-inclined thinkers in general... As fun as it is at times to think about (and things like union types do have very interesting type thoery implications), it's been my experience that trying to construct a consistent mathematical model of the TypeScript type system is more or less a fool's errand. The type system is built on years of pragmatic case-by-case decisions guided by what people are actually doing with JS/TS code in the wild, so any model you come up with to describe things is inevitably going to run into inconsistencies like this. Trying to model it mathematically is a fun diversion, but has little real-world application, IMO. |
I guess the overarching principle to keep in mind is that TypeScript is not a constraint solver. Anyway, given how narrowing works, we'd need negated types to represent what's happening here. See #42876 (comment) |
More practically speaking, I don't think discriminated-union narrowing works by elimination, e.g. even if you invert the condition so that if (t.type !== 1) {
t.f2
t2(t)
} else {
t.f1
t1(t)
} It still doesn't rule out |
Thank you both very much. For these comments and for Typescript. A number of responses, the important two at the end @fatcerberus I don't mean anything as responsible as a mathematical model so much as just a mental one. I am Mort and mere mortal here. @RyanCavanaugh Regarding #42876 (comment), I had attempted, with my discussion of escaping my confusion about Exclude (and a few other details), to communicate I had removed that mental trap from my thinking. @fatcerberus > I don't think discriminated-union narrowing works by elimination Practically, it clearly doesn't in the object attribute case ( @RyanCavanaugh I don't understand what "we'd need negated types to represent what's happening here" means. Can we get a pointer to the current implementation so that I could at least attempt to understand the meaning of the code?
Apologies that I missed the significance of this principle. Typescript is a useful tool and I'm happy to accept its behavior in this case. I'm happy to close this issue but thought that the inconsistency might be worth review. Again, thank you both and the entire team very much. |
fatcerberus: here is a simpler formulation that uses the negative conditional and works as expected in the primitive case but not in the object attribute case: type T1 = {
type: 1
f1: 1
}
type T2 = {
type: number
f2: 2
}
type T3 = 3
type T4 = number
type TA = T1 | T2
type TB = T3 | T4
const t1 = (t: T1) => {}
const t2 = (t: T2) => {}
const t3 = (t: T3) => {}
const t4 = (t: T4) => {}
const fA = (t: TA) => {
if (t.type !== 1) {
t2(t) // Argument of type 'TA' is not assignable to parameter of type 'T2'.\nProperty 'f2' is missing in type 'T1' but required in type 'T2'.(2345)
} else {
t1(t) // Argument of type 'TA' is not assignable to parameter of type 'T1'.\nProperty 'f1' is missing in type 'T2' but required in type 'T1'.(2345)
}
}
const fB = (t: TB) => {
if (t !== 3) {
t4(t)
} else {
t3(t)
}
} |
@erikerikson see the function |
Thanks so much. I am going to close this but feel welcome to reopen if it will provide the team with value. |
That’s what you say, but I remember thinking the same thing when I was first trying to internalize the complexities of TS once upon a time, and it ultimately got me interested in category theory. So just watch out for that in the future… 😉 @erikerikson If it helps at all, there is no meaningful distinction between |
It isn't exactly what happens, but you can form a good-enough mental model by thinking of narrowing as intersection: type X1 = { x: 1, y: "one" };
type XN = { x: number, y: "other" };
let obj: X1 | XN;
if (obj.x === 1) {
// Intersect (X1 | XN) with { x: 1 }
// X1 & { x: 1 } === X1
// XN & { x: 1 } === well, just that
// Neither are never
// the result is obj: X1 | XN
}
if (obj.x === 2) {
// Intersect (X1 | XN) with { x: 2 }
// X1 & { x: 2 } === never (1 & 2 is never)
// XN & { x: 2 } === well, just that
// result: obj is XN, the union is filtered
}
if (obj.x !== 2) {
// Intersect (X1 | XN) with OH NO...
// There's no type of "not 2"
// Nothing can be done
} |
This is the case. I have confirmed using type X1 = { x: 1 };
type XN = { x: number };
declare let x1N: X1 | XN;
if (x1N.x === 1) {
x1N;
}
if (x1N.x === 2) {
x1N;
}
if (x1N.x !== 1) {
x1N;
} The corresponding output in type X1 = { x: 1 };
>X1 : { x: 1; }
>x : 1
type XN = { x: number };
>XN : { x: number; }
>x : number
declare let x1N: X1 | XN;
>x1N : X1 | XN
if (x1N.x === 1) {
>x1N.x === 1 : boolean
>x1N.x : number
>x1N : X1 | XN
>x : number
>1 : 1
x1N;
>x1N : X1 | XN
}
if (x1N.x === 2) {
>x1N.x === 2 : boolean
>x1N.x : number
>x1N : X1 | XN
>x : number
>2 : 2
x1N;
>x1N : XN
}
if (x1N.x !== 1) {
>x1N.x !== 1 : boolean
>x1N.x : number
>x1N : X1 | XN
>x : number
>1 : 1
x1N;
>x1N : X1 | XN
} Thank you very much for helping me identify this and decompose it into a more tangible and detailed model! The implicitly negative case's narrowing carries a lot of explanatory power - great example to share and make it more concrete. Hope this extended documentation provides a lot of help to the community. |
🔎 Search Terms
"Exclude" "exclusion" "general" "concrete" "type"
🕗 Version & Regression Information
⏯ Playground Link
Playground Link
💻 Code
🙁 Actual behavior
The type system does not exclude type
T1
fromTA
(viaif (t.type === 1)
wheret: TA
) whenTA.type
is1 | number
but excludesT1
fromTB
whenTB.type
is1 | 3
.ALSO
The type system excludes type
T4
fromTC
(viaif (t === 4)
wheret: TC
) whenTC
is1 | number
and excludesT5
fromTD
whenTD
is4 | 5
.🙂 Expected behavior
Frankly I'm confused but I am attempting to share that to identify the problem or at least a clear model of how to understand this.
The narrowing behavior should be consistent whether the type excluded is part of a structure or not.
In more words: the else clause of
fA
should be able to determine thatt
is of typeT2
(since typeT1
is excluded) even though the type remains ambiguous in the if block OR the if and else clause offc
should be unable to determine the type oft
.My intuition is that the more useful choice would be for
T1
to be excluded fromTA
determining thatt
is of typeT2
in the else clause.Additional information about the issue
A little history: I originally confused myself by trying to declare an unknown type
Exclude<string, 'concrete'>
and looking into it came to understand thatExclude
works by removing types from a union. This broke my intuitive understanding of the class of strings and a concrete strings membership therein but was a coherent and consistent way for it to work.However, as I discovered I was wrong but also tried to simplify the situation, I discovered that
Exclude
was a distraction and that I could reproduce the behavior using conditionals but not in the way I expected. On the back of my union-type-removal model of understanding exclude I expectedfB
to also not work but found the opposite. Trying to scratch further, i removed the object structure and to my surprise found that the exclusion worked fine if the narrowing was not on an attribute of an object type.To my ignorant eyes, it seems that a corner case may have been missed. I expect it is more likely that my model is too simple.
The text was updated successfully, but these errors were encountered: