-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Design Meeting Notes, 1/3/2024 #56942
Comments
It seems like when it comes to For what it's worth: If I speak with my type theory hat on, I would really prefer it to act as a proper bottom type (everything is permitted, ex falso quodlibet, etc.), but in actual practice that might make it act too much like an |
I think there is room for carving up There's also the possibility of an "oopsies" type - a type that actually means "if any expression is deduced to be this type, then the programmer has made a mistake". I think that's something that should be introduced instead of having so many hardcoded errors in the type checker! |
This is all off-topic, but well, you've successfully nerd-sniped me 😛 I suppose in an arbitrary category a bottom object doesn't necessarily have to be an empty one - but in
That's the top type (
In a language without typed exceptions, that distinction seems academic.
This is in fact something a lot of people would like; see #23689. P.S. don't ever try to fit |
I want to push back against the identification of types and sets. A type judgement means “if you have a value of this type, here’s what you know about it”. In most type theories, you don’t have a unique empty type. In fact,
There is only one
You’re right. I mistyped.
In a language that might want to include typed exceptions in the future, it’s certainly not :-) |
Sure, types are not sets. I just meant that
There are no bottom values. That's the whole point of having a bottom type - if you think you have such a value, the program must have already diverged (stuck in an infinite loop, terminated abnormally, threw an exception, etc.). That's explicitly why it can act as a subtype of everything - because the program diverged, the code with the bottom "value" will never execute so it can safely do anything it wants with it with no ill effect. It's the type theoretical analogue of the principle of explosion; the contradiction from which to "prove" anything arises from assuming the unreachable code is reachable.
When you start speaking at this level of abstraction, it's really difficult to say what things are1. We can only really say that Anyway, all this theoretical talk is fun, but we maybe should stop clogging up a thread meant for more practical discussion of how to implement things with it. I'd recommend joining the TS Discord channel if you haven't already; then you can have discussions like this all day :) Footnotes
|
You caught me. That's what I get for not checking what I type :-). I meant "there is only one set of values of an empty type"
No. That's the point of having an empty type which is known to be empty and expressible in the type theory. A bottom type is a subtype of all types. Both Here's the practical bit. Ideally, the "intersection"/"union" should give the unique lower/upper bound of types. But the type operators
Arguably:
|
Is there any type system in existence where the (strongly typed) “subtype of all types” bottom type isn’t also empty? AFAICT that always must be the case because subtype hierarcheries diverge - the intersection of “the set of all cats” with “the set of all giraffes” is already necessarily empty, and that’s just two types, let alone finding the common subtype of everything in existence. Finding abstractions is fun, but at some point you do have to ground yourself back in reality to get any work done. |
I like to think of
Kinda. C has Most type systems don't have a bottom type e.g. you may have completely separate hierarchies for structs and for functions with no common subtype. Or e.g. there may not be a subtype relation at all! You're right that if a type system (1) has some empty type and (2) has a bottom type, then (3) that bottom type be empty.
Of course you could use that argument to say "there is no need for a The big picture problem as I see it is that so much of TypeScript is defined by its current implementation, not a language spec or some categorical characterization of how it should work. If two programmers disagree about what |
Oh for sure, I didn't mean to imply we shouldn't have abstractions at all, just that at some point you do have to apply them, and from what I could tell the conversation was headed in the direction of pure head-in-the-clouds abstraction, so I wanted to bring things a bit more down to earth by putting it in context. Kind of a "have your head in the clouds but your feet on the ground" sort of deal.
Indeed, which is why I think stressing too much over abstractions like type theory, referential transparency, etc. is misguided, at least where TypeScript-the-compiler is concerned. TS used to have a specification, a long time ago, but has since outgrown it and the direction of the project is now primarily guided by pragmatism. In the (probably paraphrased) words of @RyanCavanaugh, "consistency is a proxy measure, not an end goal" for TypeScript. By the way, you might find this interesting (or distressing, depending on your disposition w.r.t. applicability of type theory): |
I don't know if I feel like a distinction around (1) has weight as much as (2). It feels like I've definitely encountered the latter, but can't immediately come up with an example. Maybe "I promise never to call this constructor" sort of stuff with abstract classes I'm coming back to this point because I've written lots of design meeting notes where we end up claiming that "object types containing |
Oh great, we nerd-sniped one of the maintainers now 😅 |
Oh, also the UX is worse if you collapse a type containing declare function merge<const T, const U>(a: T, b: U):
{ kind: T } & { kind: U };
merge("hello", "world").kind
// ~~~~
// Property 'kind' does not exist on type 'never'.
// The intersection '{ kind: "hello"; } & { kind: "world"; }'
// was reduced to 'never' because property 'kind' has conflicting types in some constituents. |
^ and actually, I think that's a great example of why we should give an error on |
Theory vs praxis is definitely a balancing act, I'll give you that! I do think referential transparency is critically important, not just academic. I don't expect the JavaScript to be referentially transparent, but I do expect generics to result in equivalent types, given equivalent input types and context. #56906 for example seems like unreasonable behavior because of this. Here's an example where type E<X> = X extends 0 ? true : false // type E<X> = X extends 0 ? true : false
// ^?
type EU<X, Y> = E<X|Y> // type E<X, Y> = E<X> | E<Y>
// ^?
type Z1 = EU<unknown, 0> // type Z1 = boolean
// ^?
type Z2 = E<unknown | 0> // type Z2 = false
// ^? |
I don’t know what more I can say re: referential transparency other than to repeat what I’ve heard from maintainers in the past (again, paraphrasing): “it’s expected that writing different code yields different results”. While RT would be nice to have, it’s probably not a fruitful pursuit, given that. |
No, it's expecting |
Here's a good example of what I'm talking about: #51551 Quoth @RyanCavanaugh:
|
That doesn't bother me so much. An implied type attributed to an untyped expression is subject to fuzzy judgement. That's very different from a generic type expression. I do expect equational reasoning and compositionality to hold for closed-form type expressions, just as I expect for algebraic expressions. I would maybe expect some wiggle room for |
typo: |
Intersections and Unions that Depend on Instantiation Order
#56906
any
"eats" everything.unknown
does too, except forany
.SomeUninstantiatedTypeVariable | unknown
, we eagerly reduce that tounknown
.unknown
orany
.Disallowing More Property Access Operations on
never
-Typed Expressions#56780
From a purely type-theoretic standpoint, there is no issue with accessing a property on
never
, the issue is arguably more that the code is unreachable.Could argue that you should say "this whole block is unreachable, so why do you have code?"
That is too heavy-handed - the following doesn't deserve an error, does it?
Two common views of
never
arenever
to enforce an exhaustiveness check.never
to report a helpful error for a violation of the type system or usage from untyped code.In practice, this change actually breaks a bunch of existing code.
Conclusion: feels like we don't have an appetite
Preserving Type Refinements Within Closures Past Their Last Assignments
#56908
Fixes lots of issues that get duped to Trade-offs in Control Flow Analysis #9998.
When a closure is created after all assignments to some closed-over variable, then you can effectively think of that variable as unchanging.
Effectively safe to preserve the type-assignments so long as the closure isn't hoisted.
Note that we don't quite narrow in a lexical manner within "compound statements". We use the "end" of the compound statement as the source of the last assignment.
Variables that are referenced in closures past the last assignment do not necessarily trigger an implicit any warning anymore.
If we limit to
let
and parameters, then we don't have any issues with our perf suite - but it does forvar
on monaco.Function declarations in block scopes should maybe have captured variables appropriately narrowed.
The text was updated successfully, but these errors were encountered: