3 ms·
Well, sure, compared to Python, I guess TypeScript's type system is pretty great... Its type system is quite broken and I'm not going to walk back on using tha
by ragnese 5y ago
Well, sure, compared to Python, I guess TypeScript's type system is pretty great...
Its type system is quite broken and I'm not going to walk back on using that term. Sometimes the TypeScript designers/devs will explain that the brokenness is intentional. But some of the issues have nothing to do with JavaScript compatibility. For example, we all know about the conundrum of `type` vs `interface`, but did you know that an object-shaped `type` will automatically conform to a mapped type, but an `interface` wont? Playground: https://www.typescriptlang.org/play?ssl=20&ssc=1&pln=21&pc=1#code/JYOwLgpgTgZghgYwgAgLIE8CS5rycgbwFgAoZc5OALmRAFcBbAI2lIF9TSx0AHFDACq8UAXkKkKlGvWasSHEl2FosOWIggBBADbA4AZ2RiA8g2BgAPBmyR1SADTIA5E4B8nEgBMICbXCgoMHQgCGDAAPYgyDDh4QAUUDQASj7hUJ4W+mBQoADmjjIsUK4AlDQAbuHAnqTevv4oCJFZyAyqtngQNNZqnbU+fgHITSAtbUJ83egTEP31QyNj7bgaOnr6UzYrSGsGHgD0+8PhDDzA2tBGrXA1JDHxbVt2ECWkh8en55diABZwPDx0KR7nFxsJXiR3k1PhcoFc-gCgXdYqDls9dvoSkA https://www.typescriptlang.org/play?ssl=20&ssc=1&pln=21&pc=1...
Or how about the fact that basic inheritance sub-typing doesn't work?: https://www.typescriptlang.org/play?#code/MYGwhgzhAECCB2BLAtmE0DeBfAsAKFEhgGEwAXaAUwA8zL4ATGBFNTfaT6ZSgewHcAFAEoAXNABuvRA3Z4uC6MF7wIvEJQB0IXgHNBAIh4CDwjl1x5LhKNAAieqrXpM4SVOgznOAIzAAnAGsRcSkZOUUuZVV1LR19Az8g029oS0t8RHg6fwAzMGBKaAAJSAAFSgoveS4AB0rxFg98DLwsnPzCksgHXQi6hvs9Fvx8XIBXeGAyRBVuMCRa8fA6CrJBXnFSiDXhfs5eTXqKAF5oeEp+aFJ1sytRghUICk3uiF7oM+qFY-ELq96IhGeHwqEWy3IlDWGzu+EOx00SWCewA9CilLxkLVEBp-NBEDB6nlKNMQABPaAACzAtVqZIAhEA https://www.typescriptlang.org/play?#code/MYGwhgzhAECCB2BLAt...
- zarzavat 5y agoWho cares if TypeScript's type system is unsound if it lets you express things that you cannot express in a nominative system like Java's. I get so much more productivity out of TypeScript's type system than any other language including "serious" type systems like Haskell's. Moreover TypeScript's type system is so powerful it actually catches a good number of bugs that Haskell would never catch, because it does control flow analysis by default. For many languages you would need to install a separate static analyser (if one even exists), having CFA as part of type checking in your editor is godlike. Sure if you are writing software for a radiotherapy machine, don't write it in typescript! Use whatever proof assistant takes your fancy. But if bugs are not literally going to kill people, TypeScript's type system is about the best in existence.
- saagarjha 5y agoBecause type system unsoundness leads to real bugs when people (quite reasonably) make assumptions based on the belief that it is sound. I think on the whole TypeScript does an OK job (considering that most languages have unsound type systems too, including Java) because it’s certainly better than nothing but it’s still something to watch out for.
- afiori 5y agoIt would be nice if typescript had a option to compile with (obviously limited) runtime checks
- ragnese 5y ago> Who cares if TypeScript's type system is unsound if it lets you express things that you cannot express in a nominative system like Java's. I get so much more productivity out of TypeScript's type system than any other language including "serious" type systems like Haskell's. If you enjoy your type system being more expressive at the cost of soundness, you should try this language called JavaScript! It lets you express things that you can't in TypeScript! > Moreover TypeScript's type system is so powerful it actually catches a good number of bugs that Haskell would never catch, because it does control flow analysis by default. For many languages you would need to install a separate static analyser (if one even exists), having CFA as part of type checking in your editor is godlike. The control flow analysis has nothing at all to do with the soundness of the type system. Kotlin has similar with it's "smart casting," but that doesn't require it to have an intentionally unsound type system.
- epolanski 5y agoI’ve been using typescript for years, and I’ve rarely met these un soundness issues. I agree that it has its limits, and that there are better type systems, but compared to JavaScript it’s a night and day experience. The amount of help and clarity it brings on large codebases has no equivalent in JS world, and even tho I love Scala or Elm they are simply not realistic alternatives to JS for most of the industry.
- phailhaus 5y agoBoth your examples are very interesting, in that I can't figure out how they would actually get hurt by these issues when writing code. For your `type` vs `interface` example, is the fear that you won't be able to use a type when a function takes a `Record` type? It's an inconvenience for sure, but that won't inadvertently introduce a bug. And for your 'inheritance sub-typing' example, can you explain what the "right" behavior would be? It's a function that mutates properties on an object, how is the type system supposed to detect that? That's already a code smell and should be avoided.
- ragnese 5y ago> For your `type` vs `interface` example, is the fear that you won't be able to use a type when a function takes a `Record` type? It's an inconvenience for sure, but that won't inadvertently introduce a bug. Right. That one, by itself, won't introduce a bug. But it's just another weird thing to remember about TypeScript's type system being inconsistent. I bumped in to it because I wanted to define my own JSON-like type for use as query params being sent to a particular API that has a custom syntax for certain things (like passing an object as a query param, etc). So I wrote a type something like this: type ExtendedJson = boolean | number | string | null | undefined | OurCustomTimestampType | SomeOtherCustomTypeWeUse | ExtendedJson[] | ExtendedJsonObject type ExtendedJsonObject = Record<string, ExtendedJson> And then in my function that sends requests to the API in question, I require a parameter like `queryParams?: ExtendedJsonObject`, and I have a function that knows exactly how to format this ExtendedJsonObject into the right string for the request. Neato. Except then, I realized that most of the specific query types I wrote were interfaces. But I only got compile errors in some places and not others. I had no idea why some of my types worked and some didn't. The error message was clear enough, I guess. It said that the type didn't conform to `{ [string]: ExtendedJson }`, but I coudln't figure out why some of my types did conform and some didn't. What's worse is that some of my query types that were defined as interfaces DID work without a compile error! In hindsight, I now understand that the reason those worked was because I was actually "wrapping" those interfaces into types with things like Readonly<FooQueryParams> or Omit<BarQueryParams, 'someKey'>. So those were now types instead of interfaces, but it wasn't an obvious enough clue for me, so I spent hours trying to figure it all out. If that were the only weird thing, I wouldn't still be bitching about it, but in the last year that I've been focusing on this frontend project, I feel like I've spent countless hours learning about holes and inconsistencies in TypeScript. It feels like every single week I learn about some other broken or inconsistent nonsense that I just think I shouldn't have to deal with. > And for your 'inheritance sub-typing' example, can you explain what the "right" behavior would be? It's a function that mutates properties on an object, how is the type system supposed to detect that? That's already a code smell and should be avoided. The correct behavior is that it should be an error to pass a HasDog to a function that requires a HasAnimal. { pet: Dog } is NOT a sub-type of { pet: Animal } just because Dog is a sub-type of Animal. The type theory concept is called "variance". You have "covariant", "contravariant", and "invariant". Mutable object and array types are invariant in their field types. So you can't pass a Array<Dog> for an Array<Animal> because adding an Animal to an Array<Dog> is a type error: an Animal is not necessarily a Dog. However, you CAN pass an immutable Array<Dog> for an Array<Animal> because you are guaranteed to only be reading the elements as Animals, and a Dog is an Animal. Return types often have the inverse type relationships to input params: You can return an Array<Dog> for an Array<Animal> return type because the caller who receives the returned value will treat it as an Array<Animal>, so it's okay to add a Cat to it, even though it was originally only Dogs. And, while I agree that mutating an input param is generally a bad idea, that's no excuse for the type system to not be correct. Every other statically typed language is going to handle this correctly (except primitive arrays in Java. They are also unsound, like TS, but, in Java's defense, it's only primitive arrays, and you almost never use arrays directly in Java- you use List<T>, which is not wrong/unsound). When I say every other statically typed language, I'm literally talking about every statically typed language I've ever used: C++, Java (again, except arrays), Go, Swift, Rust, Kotlin, Scala, even PHP. They all handle this correctly, because it's really basic type theory.