|
Falcon2001 posted:honestly I do kind of wish that Python just had a typing-enforced version Kinda in the same boat, but then again there's some quirks of the type hinting system that have made me just throw my hands in the air before. Apparently my biggest gripe was recently fixed in mypy, but is demonstrative: https://github.com/python/mypy/issues/5876
|
# ? Sep 26, 2022 10:10 |
|
|
# ? Jun 5, 2024 21:20 |
|
LOOK I AM A TURTLE posted:Unless I'm misunderstanding you, this one doesn't seem to have been applicable for a while: https://www.typescriptlang.org/play...AE1AG50uYCGkiAA. It works at least as far back as TS 3.3, which is over three years old. I think the type unification was improved somewhere around that time. Oh no your example is 100% fine. but here comes the footgun: In your example you have: code:
code:
|
# ? Sep 26, 2022 10:19 |
|
champagne posting posted:Oh no your example is 100% fine. but here comes the footgun: Ah. In my defense, I had that in my example because you had it in your example.
|
# ? Sep 26, 2022 10:40 |
|
LOOK I AM A TURTLE posted:Ah. In my defense, I had that in my example because you had it in your example. It was a while ago I hope I misremembered. It's a big fat gun for our feet nonetheless
|
# ? Sep 26, 2022 10:43 |
|
That doesn't seem like a footgun at all. In fact it's the opposite of a footgun. A footgun would be if it allowed you to do something unsafe without realizing it. This is disallowing you from doing something (ostensibly) safe while explicitly telling you so. And from a type theory point of view, it makes sense. The union type is either an object that is statically known to be beer, or statically known to be wine. You can't use values to discriminate between types at runtime without a type system that allows for dependent typing, which typescript (and most other languages) don't have. If you actually want runtime polymorphism in Typescript, then you could use a class: Playground Link edit: and also if you're drat sure it's safe, then of course you could just cast manually. With great power comes great responsibility and all that Volte fucked around with this message at 14:02 on Sep 26, 2022 |
# ? Sep 26, 2022 13:57 |
|
Carbon dioxide posted:If TypeScript isn't strongly typed enough for you, instead you can use tooling that compiles a strongly-typed language directly into Javascript. Scala.JS comes to mind, although I've never had the chance to work with it myself so I'm not sure how well it holds up. There's also Rescript, an OCaml to JS transpiler.
|
# ? Sep 26, 2022 14:38 |
|
Is the complaint here that they're expecting Typescript to duck-type (which it can do to a limited degree with interfaces specifically) but then are surprised when it also tries to enforce strong typing at runtime despite the duck typing? Interfaces define the shape of a class (not the data or logic), and you're specifically defining the two child interfaces to have the shape that includes a field with a property that has only one possible value and isn't optional. If you try to union the two interfaces (with conflicting shapes), the result is a broken shape because it needs to simultaneously contain the same field with two different values. Union of two interfaces does not lead to each property being a union of the equivalent properties from each interface; that would be insane and counter-intuitive. It would have no ability to figure out what the correct types are half the time. You have the ability to do this yourself though, even though it defeats the point: code:
Basically, a union is not the same thing as referencing sub-classes by their base class; it is defining a new "type" that is the union of the classes (so it can hold either of the two). Specifically from the docs: "TypeScript will only allow an operation if it is valid for every member of the union." That said, the fact that TS allows duck typing like this is an unavoidable horror and I hate it. It's not a foot-gun, just a big siren that tends to go off whenever you press the wrong button until you figure out that the similarly coloured button next to it is the right button to press in fact. Over subsequent versions, the siren now even tells you what the problem is and how to avoid it, as well as making it pretty much painless to switch from one to the other (unless the problem is in a module/library). e: corrected the example which just also highlights why it's a bad idea. You can't avoid defining beer: true or beer: false because an interface is not the same thing as a class. Red Mike fucked around with this message at 14:49 on Sep 26, 2022 |
# ? Sep 26, 2022 14:46 |
|
I think this is a decent example of the limitations of Typescript's duck-typing inference:TypeScript code:
|
# ? Sep 26, 2022 16:04 |
|
I'm losing my mind here. The earlier example does work from compiler version 3.5 and onwards, even with only type Glass = BeerGlass | WineGlass and beer: true/false on the interfaces.. I could've sworn I tried it earlier and that it failed, but it works. The change is named very explicitly in the 3.5 documentation: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking under "Smarter union type checking". The reason it works is because true and false are all the possible values of boolean, so the compiler is essentially able to turn { beer: true } | { beer: false } into { beer: boolean }. It can do the same thing with any other enumerated type when it can see that you've exhaustively enumerated every possible value. Am I still missing something? Try switching between 3.3.3 and 3.5.1 here: https://www.typescriptlang.org/play...QsWoOxKnuHunWQA. It works in 3.5.1 and on, and says "type 'boolean' is not assignable to type 'false'" in 3.3.3. Red Mike posted:Union of two interfaces does not lead to each property being a union of the equivalent properties from each interface; that would be insane and counter-intuitive. It would have no ability to figure out what the correct types are half the time. It does when the fields exist in all the unioned types, so { type: 'A' } | { type: 'B' } | { type: 'C' } is equivalent to { type: 'A' | 'B' | 'C' }. Or even this: { foo: 'Foo', bar: 'Bar' } | { foo: 'Bar', bar: 'Foo' } is equivalent to { foo: 'Foo' | 'Bar', bar: 'Foo' | 'Bar' }. Red Mike posted:This is literally what you're trying to do, which is to have the property "beer" be a union of true and false in the base type that you can use to store the object in. But instead of making the property "beer" a union, you're trying to make a union of two object types that both define a property with different types. I believe the idea was to have "beer" be a union of true/false -- which is exactly equivalent to the boolean type -- only on the union type Glass and not on the base type BaseGlass. I don't see anything wrong with defining the type that way. NihilCredo posted:
thirst2 does compile in version 3.5+. The piece variable indeed doesn't work in any version. I assume it's because while the compiler now knows that boolean is equivalent to true | false, it doesn't know that !true is equivalent to false and !false is equivalent to true. That's something that could theoretically be added. Edit: Scratch that last part. The compiler actually does know that !true is false and vice versa, so the type of const x = !true is false, not boolean. As you said the reason it can't tell that the diagonal/straight variables are guaranteed to be correct is deeper, and would require something like dependent types. LOOK I AM A TURTLE fucked around with this message at 16:27 on Sep 26, 2022 |
# ? Sep 26, 2022 16:13 |
|
LOOK I AM A TURTLE posted:thirst2 does compile in version 3.5+. The piece variable indeed doesn't work in any version. I assume it's because while the compiler now knows that boolean is equivalent to true | false, it doesn't know that !true is equivalent to false and !false is equivalent to true. That's something that could theoretically be added. Interesting, they did add some better handling apparently. However, it's not about the 'not' operator. This still doesn't compile in 4.8.2: TypeScript code:
|
# ? Sep 26, 2022 16:22 |
|
NihilCredo posted:However, it's not about the 'not' operator. This still doesn't compile in 4.8.2: Yeah, you're right. The compiler does in fact know that !true is false and !false is true, but it's not enough.
|
# ? Sep 26, 2022 16:30 |
|
LOOK I AM A TURTLE posted:I'm losing my mind here. The earlier example does work from compiler version 3.5 and onwards, even with only type Glass = BeerGlass | WineGlass and beer: true/false on the interfaces.. I could've sworn I tried it earlier and that it failed, but it works. The change is named very explicitly in the 3.5 documentation: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking under "Smarter union type checking". The reason it works is because true and false are all the possible values of boolean, so the compiler is essentially able to turn { beer: true } | { beer: false } into { beer: boolean }. It can do the same thing with any other enumerated type when it can see that you've exhaustively enumerated every possible value. My bad, it looks like they fixed the boolean/all values specifically enumerated edge cases. That doesn't change that the entire approach is trying to do what is basically classes and sub-classing, except it's doing it via interfaces (duck-typing) and unions. Sure, you can do it, but that's not the right tool for the job. Highlighted by the fact that they deliberately had to add handling for these cases and are unable to handle the general case. Because it's the wrong tool for the job. Don't get me wrong, in TS all too often you'll end up having to use that particular tool, because an interface is what you have to use (or what a library provides, or what a tool expects, etc). But it's not a slight against the language that it's letting you hammer in a screw but it doesn't work that well all things considered. The closest thing to a slight is that interfaces/unions are too readily available for things that you should use classes/other types for, but that's vague and probably not solvable. e: I'll be honest, more of a horror is literal types altogether because they're a hacky fix that's been taken to a ridiculous extreme. There just shouldn't be a way to define an interface as "type false", only "type boolean".
|
# ? Sep 26, 2022 16:34 |
|
I assume that's because there's no way to even write the type of 'diagonal' or 'straight' independently in the union context, so they can't be unified independently and it falls back to the "all operations must be valid on all union branches" thing, so 'b' would have to be assignable to both Queen and Knight. It's not enough to say that they are both booleans, because then true/false and false/true would be valid.
Volte fucked around with this message at 16:42 on Sep 26, 2022 |
# ? Sep 26, 2022 16:39 |
|
LOOK I AM A TURTLE posted:
... no? Sorry, I haven't even touched Javascript in years, I'm not sure I'm seeing the problem. Will it not print out: "Your foot has been shot!"?
|
# ? Sep 26, 2022 17:01 |
|
Red Mike posted:My bad, it looks like they fixed the boolean/all values specifically enumerated edge cases. That doesn't change that the entire approach is trying to do what is basically classes and sub-classing, except it's doing it via interfaces (duck-typing) and unions. Sure, you can do it, but that's not the right tool for the job. Highlighted by the fact that they deliberately had to add handling for these cases and are unable to handle the general case. Because it's the wrong tool for the job. It's been awhile since I spent serious time in TypeScript (or JavaScript), but promoting class inheritance and decrying literal types seems to entirely miss the point of TypeScript. Maybe your problem isn't wrong tool for the job, it’s that you've actually got a different job on your hands?
|
# ? Sep 26, 2022 17:26 |
|
Red Mike posted:My bad, it looks like they fixed the boolean/all values specifically enumerated edge cases. That doesn't change that the entire approach is trying to do what is basically classes and sub-classing, except it's doing it via interfaces (duck-typing) and unions. Sure, you can do it, but that's not the right tool for the job. Highlighted by the fact that they deliberately had to add handling for these cases and are unable to handle the general case. Because it's the wrong tool for the job. I'd say it's quite debatable if it's the right or wrong tool. Many languages don't have classes at all, so their only way to implement polymorphism is with algebraic data types. TypeScript offers both, but personally I prefer discriminated unions to classes in many cases. We could go into a big debate about the pros and cons of OOP here, but all I'll say is that there's a reason the industry has started to move away from subtyping in the last decade or so. Subtyping has many things going for it, but it also has a number of disadvantages. Also, I know this is a very pedantic point, but TS interfaces are structurally typed, not duck-typed. You can turn any object into an ad-hoc type, but you can't access data on an object unless you said that it's there. If duck typing is "if it quacks like a duck", structural typing is more like "a duck is whatever I say it is". Absurd Alhazred posted:... no? Sorry, I haven't even touched Javascript in years, I'm not sure I'm seeing the problem. Will it not print out: "Your foot has been shot!"? It will, but I wanted to shoot my enemies, not my own foot. The shoot function was not supposed to be able to take a foot as input, but it's embarrassingly easy to do it anyway. The layer of safety added by TypeScript is very thin compared to languages that don't have (almost) full type erasure at runtime.
|
# ? Sep 26, 2022 17:32 |
|
LOOK I AM A TURTLE posted:It will, but I wanted to shoot my enemies, not my own foot. The shoot function was not supposed to be able to take a foot as input, but it's embarrassingly easy to do it anyway. The layer of safety added by TypeScript is very thin compared to languages that don't have (almost) full type erasure at runtime. My man you literally wrote "as any" in your example. It's hard to complain that 'the layer of safety is very thin' when you specifically used the keyword whose only and entire purpose is to disable the type checker.
|
# ? Sep 26, 2022 17:42 |
|
LOOK I AM A TURTLE posted:It will, but I wanted to shoot my enemies, not my own foot. The shoot function was not supposed to be able to take a foot as input, but it's embarrassingly easy to do it anyway. And frankly any example that does use "as any" is a poor indictment of the type system, as it is explicitly designed to escape from the constraints type system. Volte fucked around with this message at 17:48 on Sep 26, 2022 |
# ? Sep 26, 2022 17:44 |
|
Volte posted:It's not a violation of the type contract because Foot is a structural subtype of Enemy. Woops, you're right. I should've put some other field on Enemy to differentiate them. Anyway, I know it wasn't a very impressive example. I was just trying to point out how the type system of TypeScript is quite different from that of languages like C# and Java, where it's practically impossible to put a string inside an int or whatever. I'm actually a big fan of TypeScript, but I've also had a few too many experiences of my variables not containing what I think they do, because that "as any" part was hidden in some deep dark corner of the code.
|
# ? Sep 26, 2022 18:05 |
|
So in C++ if I define:Cpp posted:struct JustAString then a function that accepts JustAString&s would refuse to compile with AStringAndAnotherThing&, even though theoretically I could have also written: quote:struct AStringAndAnotherThing : public JustAString
|
# ? Sep 26, 2022 19:10 |
|
Absurd Alhazred posted:So in C++ if I define:
|
# ? Sep 26, 2022 19:19 |
|
Absurd Alhazred posted:So in C++ if I define: TypeScript code:
LOOK I AM A TURTLE fucked around with this message at 19:27 on Sep 26, 2022 |
# ? Sep 26, 2022 19:22 |
|
LOOK I AM A TURTLE posted:It does enforce nomimal typing for class objects, but not for "plain objects". edit:
|
# ? Sep 26, 2022 19:28 |
|
If TypeScript doesn't change its behavior if I change something then I'm going to call it "it doesn't see it". Eugh, I just hope I never get pushed into webdev.
|
# ? Sep 26, 2022 19:31 |
|
Volte posted:This isn't the case. Typescript doesn't distinguish between classes and other types, and your first example would compile fine if you passed the correct number of arguments to the B constructor. The explicit inheritance mechanic is part of JavaScript's class system and is necessary to maintain that inheritance relationship at runtime, but the same structural subtyping rules apply regardless of inheritance. Yeah, I was way off. It just goes to show how little I use classes in TS.
|
# ? Sep 26, 2022 19:31 |
|
FlapYoJacks posted:Python is the best non-compiled language there is. Rust is the best compiled language. You misspelled Lisp. Twice.
|
# ? Sep 26, 2022 19:33 |
|
Absurd Alhazred posted:If TypeScript doesn't change its behavior if I change something then I'm going to call it "it doesn't see it".
|
# ? Sep 26, 2022 19:33 |
|
Canine Blues Arooo posted:Does Rust have any good GUI Frameworks? Not yet. Rust is excellent for command-line programs, at the moment.
|
# ? Sep 26, 2022 19:42 |
|
Volte posted:It's the exact same relationship as in C++ if the inheritance was specified. It's not that it can't see the difference between the base class and the subclass, it's just that they are both compatible with that particular parameter. If you tried to implicitly cast the other way, both languages would fail to do it for the same reason. That's exactly it, though: I don't want my compiler/interpreter helpfully finding these underlying structures for me, I want it to raise the alarm that I passed something in that wasn't explicitly allowed.
|
# ? Sep 26, 2022 19:48 |
|
Absurd Alhazred posted:That's exactly it, though: I don't want my compiler/interpreter helpfully finding these underlying structures for me, I want it to raise the alarm that I passed something in that wasn't explicitly allowed. There are some ways to enforce nominal typing (for example) but I wouldn't say it would be desirable all the time.
|
# ? Sep 26, 2022 20:23 |
|
Volte posted:You are explicitly allowing it by defining the structure that way. It's not a helpful compiler shortcut, it's a fundamentally different kind of type system. The name of a type simply does not matter beyond documentation. You don't even need to use the name of the type to reference it, you can directly write out the structure. If you actually want to enforce name identity, you'd have to build that into the structure of the type. You keep using the word "explicit". I don't think it means what you think it means.
|
# ? Sep 26, 2022 20:33 |
|
Typescript is a structural-by-default typing system, the opposite of c++. It is like if all c++ functions were templates you provide various levels of constraint to. It's not that complicated
|
# ? Sep 26, 2022 20:41 |
|
Absurd Alhazred posted:You keep using the word "explicit". I don't think it means what you think it means.
|
# ? Sep 26, 2022 20:46 |
|
it seems reasonable enough for me? Like even in C++ you can use typedefs to get pretty similar and useful behavior. A size_t and uint64_t are the same thing other than the name, but listing "size_t" in the function signature communicates to the user how the function is supposed to be used, even if it doesn't completely restrict them from using it wrongly
|
# ? Sep 26, 2022 20:48 |
|
Volte posted:If you define a function that says "I will accept any object that has a string called 'name'" and then you define an object that has a string called 'name', that's being pretty explicit about the conformance of one thing to the other. But that's not what I'm doing. I'm saying "I accept SpecificallyThisObject", that happens to have a string in it. I don't want it to accept "ThatEntirelyDifferentObjectThatHappensToStartWithAString" without me explicitly enabling that behavior somehow because it detects that there's this structural similarity and just "helpfully" does that for me. Phobeste posted:Typescript is a structural-by-default typing system, the opposite of c++. It is like if all c++ functions were templates you provide various levels of constraint to. It's not that complicated Yeah. That's why I said that TypeScript doesn't see a difference between things that C++ does, so I'm not sure why this argument is continuing. If I didn't explicitly state something and it happens, then it happened implicitly. I don't care why it's happening implicitly.
|
# ? Sep 26, 2022 20:56 |
|
No. As with a c++ template, you state "I accept specifically an object that has this shape" that explicitly has a string in it with explicitly that name. The compiler "detects" (in scarequotes because it's really not any different from "detecting" that an object has the same class definition identity) that an argument matches the structure of what you've explicitly asked for, and allows it. This is not any less strict or less sound than nominative typing. It is just different. There's not an argument, you're just incorrect.
|
# ? Sep 26, 2022 21:05 |
|
Absurd Alhazred posted:But that's not what I'm doing. I'm saying "I accept SpecificallyThisObject", that happens to have a string in it. I don't want it to accept "ThatEntirelyDifferentObjectThatHappensToStartWithAString" without me explicitly enabling that behavior somehow because it detects that there's this structural similarity and just "helpfully" does that for me.
|
# ? Sep 26, 2022 21:06 |
|
Phobeste posted:No. As with a c++ template, you state "I accept specifically an object that has this shape" that explicitly has a string in it with explicitly that name. The compiler "detects" (in scarequotes because it's really not any different from "detecting" that an object has the same class definition identity) that an argument matches the structure of what you've explicitly asked for, and allows it. This is the "if you access our website you have signed the EULA" rider of type systems. Volte posted:If you don't want it to accept that then you should not define your intended interface in a way that unambiguously has a subtype relation with it. You claim you're saying "I accept SpecificallyThisObject" but in TypeScript, you are not saying that because it can't be said. You can encode a nominal identity inside the structure (like { name: string, kind: "SpecificallyThisObject" }) if you want, but at the language level, the name of the type is not important to type checking, full stop. So what I said: Absurd Alhazred posted:So in C++ if I define: is correct. Why are still arguing with me? Absurd Alhazred fucked around with this message at 21:09 on Sep 26, 2022 |
# ? Sep 26, 2022 21:07 |
|
It's incorrect because you can't pass JustAString in a place where AStringAndAnotherThing is expected. If it didn't see a difference between them, that would obviously not be the case. There's a big difference between "A and B are both acceptable" and "A and B are indistinguishable". edit: I think I misunderstood by what you meant by "these two". I thought you meant the types, not the two examples. I guess we're arguing for nothing.
|
# ? Sep 26, 2022 21:16 |
|
|
# ? Jun 5, 2024 21:20 |
|
Volte posted:It's incorrect because you can't pass JustAString in a place where AStringAndAnotherThing is expected. If it didn't see a difference between them, that would obviously not be the case. There's a big difference between "A and B are both acceptable" and "A and B are indistinguishable". I meant between the two instances of AStringAndAnotherThing. The one that explicitly inherits from JustAString vs. the one that just structurally contains a prefix equivalent to JustAString. C++ will reject the latter up-conversion unless I use something explicit like reinterpret_cast (which is not recommended because I want it to force me to think about why I did or didn't perform this inheritance).
|
# ? Sep 26, 2022 21:25 |