TypeScript’s quirks: How inconsistencies make the language more complex
blog.asana.com
blog.asana.com
I get how folks coming from "natively" static type systems could find all this weird, but I think by targeting "JS but with better checks" instead of a more fundamental redesign, the TypeScript crew have managed to build something that can get traction where other efforts have not.
That would be one of the hello-world test cases for a type checker bolted on to a dynamic language.
That type checker itself has introduced the syntax for declaring Cat and Dog classes; yet is neglecting to do the obvious with it.
* All fields of Dog are also present in Cat, with compatible type signatures
* All functions of Dog are also present in Cat, with comaptible type signatures
"compatible type signatures" does seem to leave some suprising co/contravariance holes still when using wider union types, but C#/Java arrays also have co/contravariance holes and we don't automatically write off their entire type systems because of it. Or, well, at least I don't ;).
As a meaningless point of ancedata: Making these types sufficiently equivalent to fall through this type safety hole appears to be rare enough that I've never done so by accident - allowing me to be suprised by the article's example of passing a class Cat to a class Dog-accepting function being legal. While typescript has never been my primary dayjob, it's been a significant secondary part of my dayjob and hobby junk.
(EDIT: Minor clarifications)
Still much better than working in C or some other weakly typed languages.
It’s just a different kettle of fish to learn.
However:
- The language primarily uses structural typing. If two classes are compatible, it's not completely unreasonable to expect them to also behave in a structural way (though I would prefer them not to).
- Adding a private field to a class makes it work as a nominal type (https://michalzalecki.com/nominal-typing-in-typescript/#appr...). It feels a bit hacky and it's not very well documented, but you could argue that until a class has some private state you can't publicly access, there's no reason to prevent you from declaring a compatible class.
- From my experience idiomatic TypeScript rarely uses classes, the primary exception being React components. In teams I've been a part of we've always written TypeScript (and JavaScript) like an (impure) functional language, taking full advantage of ADTs, simple structurally typed data and more-or-less pure functions.
If you have compatible method, fields and types, in a duck-typed ecosystem like Javascript it is a major boon that it is possible to interchange them, because it happens all the time in the ecosystem.
The standard way to do enforce exactly the right interface in typescript if you need to is to add a `type: "cat" | "dog"` field to your Animal interface and have the Cat interface have `type: "cat"`. If works well and is very similar to a JVM Class object or .NET Type object.
Structural typing, literal types and dependant types are major blessings to have available sometimes, and I'd really like languages to adopt some of Typescripts power and simplicity in this. So far I've only seen Julia have some of it, although Scala 3 also introduces some bits.
const cat = new Cat();
if (cat instanceof Dog) console.log('this should never be possible!');
What is this strange protective behavior towards TS, can we stick to logic please?That has some downsides; it can be abused to create a ball of mud.
However, a middle-of-the-road type check which insists that an object must have all of the properties of Dog, even ones you're not using, isn't very useful. It kills the above dynamism, and yet allows abuses to sneak through.
You will not find that cats are being passed to Dog functions until you try to maintain Dog. And then you will discover that, oops, everything you add to Dog has to be replicated in Cat.
Basically, a typecheck which says that an object coming in has to have all the properties of Dog might as well just be a subclass check. The smart way to ensure that a non-Dog class has all the properties of Dog is inheritance.
A properly done static version of this structural type check would validate that the object being passed to the function doesn't have all the properties of a Dog, but that it has all the properties which just that function requires (including transitively: through any functions it calls).
https://en.wikipedia.org/wiki/Duck_typing#Structural_type_sy...
> Duck typing is similar to, but distinct from structural typing. Structural typing is a static typing system that determines type compatibility and equivalence by a type's structure, whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time.
With structural typing, the type system will check that the "duck" not only quacks but also looks like and walks like a duck, before it is allowed to be asked to quack.
EDIT: of course with small enough interfaces you can get structural typing pretty close to duck typing, if you have interfaces "QuacksLikeDuck", "WalksLikeDuck", "LooksLikeDuck", etc. instead of one big "Duck" interface.
However sometimes I kinda miss some functional stuff that you can for example find in Scala or Rust, like pattern matching. Specifically, we've had experience using/developing an input validation library for the backend in Typescript (https://github.com/StrontiumJS/Framework/) that is composable and also returns the correct type in one go. We used a lot of try catches for that, but that makes validation slow. So we started using "maybe promises" which are similar to Scala's Futures with Success/Failure. But the fact you don't have pattern matching in Typescript like in Scala makes it a bit ugly.
You mean Microsoft managed to get traction? It seems most TS proponents don't realize TS is a success because they fell for smart marketing and serious money behind the project, not because it's a better language than 'other efforts' like for example Purescript or Dart.
But every comment I make against TS seems futile. TS proponents defend their little language very fiercely regardless of all the flaws it has. It's almost worth a study how Microsoft managed to do that. Overall a real pity, because there are so many better options and ideas for those who want static typing. TS is actually the worse choice IMAO.
It's almost as if Microsoft learned the techniques of linguistic evangelism from Sun when they studied the ideas behind Java to design C#, but they left out the anti-linguistic-miscegenation Java-supremacist ideals of "100% Pure Java [TM]" when they made it easy to integrate other mongrel languages with COM and P/Invoke.
With my iterative test-driven approach, I probably iterate about once every minute on average (also, my development iteration time gets faster as I become more experienced in the project). So 30 seconds represents 50% of my total development time. Meaning that I could have written 50% more tests or 50% more features in the same amount of time. And that's not even accounting for my loss of focus incurred as a result of waiting 30 seconds to see the result of my code change. By the time I see the result, the concept in my mind is not as fresh as it was 30 seconds earlier.
Combined with other live-reloading tools like nodemon or jest --watch you can have almost the same speed of your development-test-run-debug loops as in plain JS. But you do gain an additional instant feedback channel from the compiler.
Let us know when you are willing join the rest of us in the real world.
I have many stories on the subject but most recently I did a significant refactoring for a JavaScript project (a popular open source library I created) to move from callbacks to async/await and was able to re-use pretty much all of the existing integration test logic with only aesthetic changes (to account for the new async/await API).
When I finished the refactoring, I had modified at least 70% of the entire source code of the project and I took it as an opportunity to make significant structural changes to my code to align with the new async/await flow in the most optimal way possible. This kind of big refactoring would have made code highlighting completely redundant and would have prevented me from looking at the problem with a fresh mind and taking into account the new features offered by async/await. It was a very successful refactoring; no new issues were uncovered after the point that I managed to get all the original tests passing again and the code was a lot simpler than before.
I think TS has a way of locking down old structures in a way that make it sub-optimal in the long run. Feature-oriented tests are by far the best way to ensure that product quality is maintained after a refactoring IMO.
In a project with >90% coverage, I would find refactors dreadful. After any substantial change beyond adding stuff, there were always a myriad of tests failing. Not hard to fix, but simply tedious and dreadful.
On the other hand, I have an Elm project with no unit test, only some e2e tests. I can do huge transversal changes, and I don't find ot tedious at all. It gave me back the fun in programming. The Elm project is a game, so there should me an order of magnitude more bugs, but the opposite was the case.
I write unit tests once the project is stable and usually only for parts of the logic that are either very complicated or operationally critical. I try to avoid implementing 'very complicated' modules as much as possible.
Unit tests can't really lock down code, or then we shouldn't call those "unit" anymore.
- Living documentation: types are a clear description of how your code and its data are laid out, and this structure is kept always up-to-date with the code, regardless of future changes. If the "documentation" (the typing) is wrong, the compiler will complain.
- Better tooling: autocomplete, more robust auto-refactoring
- Elimination of an entire class of bugs. This class may include: 1. `Cannot read property 'foo' of undefined` 2. forgeting a case in a switch statement 3. `'foo' is not a function` 4. in general, your code receiving data that isn't laid out in a way you expected
(Property typos is one within this class of bugs, but it is not all of it)
Yes tests catch these bugs as well. But tests are also manually written. And often not written at all. A compiler makes these (mundane) sanity checks automatic and mandatory.
---
Typescript's slogan is "JavaScript that scales" because static typing improves: dev documentation (via types living inside the code) and dev tooling (refactoring and autocomplete), both of which help immensely as a project grows in age (devs become forgetful), team size (new developers get lost), and convolutedness (e.g. tech debt).
auto-refactoring.. yeah we all trust that. Until you come to the boundaries where all the types don't exist, like from the client to the server, to the database via its message system, etc. All the stuff coming in and out of your code is untyped.
Autocomplete works without typescript, even if it uses "the typescript engine". The types aren't needed.
Eliminates an entire class of bugs that nobody has ever had a problem with.
Until you automate building TypeScript definitions from your external code. We do this to varying degrees and are always trying to improve.
What makes you say so? I find it more readable than javascript.
Regarding typing at the boundaries, this is precisely where typescript shines. The types exist whether they are documented explicitly in your code (in the form of interfaces, types, protocols, etc), or not. This is where the documentation aspect come in to play - it is far easier to read a type definition than having to chase up API documentation, mentally parse tests, or console logging out api responses, to understand your boundary. Your refactoring argument is a strawman, because you wouldn’t just go and refactor names of things at the boundaries of your code in typescript, or any other language, without understanding the API spec. What typescript gives you is precisely the ability to refactor at the boundary when the spec changes and be much more confident that your changes aren’t going to break a whole bunch of thing, especially when it comes to the “class of bugs” that literally every JavaScript developer has dealt with, whether you are aware of them or not.
For me, TypeScript really started to shine once I started taking advantage its generic support, higher-level utility types like Pick<> and Optional<> (and really anything involving keyof), and creating static type checks via the unknown and never types.
This is especially effective when working on legacy JS code bases where unit tests and other good engineering principles were not used well, or at all.
My typescript compiler catches typoed property names every time.
> I can't remember the last time I merged code with a typo in a property name.
I can't remember the last time my newly written code made it into the runtime/browser with a typo in a property name.
> ...if you typoed a property name and your tests didn't catch it, it means that either your tests are poorly written or your object didn't need that property to begin with.
The types are much more than just property names.
Its such a powerful pattern, I'm a bit surprised to see so much negativity about it. I'm aware there's alternatives and reasoned opinions, but I bet a significant number of people have never seen or programmed in this style and aren't aware of what they are missing. For me, I can't imagine using another typed language that doesn't support implicit interfaces in this manner.
As someone who learned TypeScript just by using it and without any serious "study", I'll have to disagree the basic premise of the article.
I already knew about almost everything the article mentioned and nothing really seemed weird to me at the time.
The only point that I did not know about is type narrowing not working on nested objects, because that came up exactly 0 times in about 50k lines of TypeScript so far.
Half of the points made are actually TypeScript being lenient (interfaces allowing excess properties, classes working like interfaces). In my book this makes the TS easier to work with, because it gets out of the way when it's not needed.
Classes essentially working like interfaces is probably a result of TS wanting to be backward compatible with JS, because when you enrich your existing JS lib with type information, TS can't know about your "classes" - they're a runtime thing. Treating them the same as interfaces can make this much nicer.
It also allows 3rd party code to add extra properties to your new-instanciated object and using an interface to reflect the change, but still being able to pass it to stuff expecting the original thing. This way you can have type safety without losing JS features.
No–this is a very surprising and unsound choice and not at all what you would expect if you previously saw that TypeScript lifts classes into types.
> This way you can have type safety without losing JS features.
IMHO, you can't get type safety out of an unsound type system. (Which TypeScript is, by choice.) I think surprises like the above drive that point home.
JavaScript will never be fully type-safe without fundamental changes, which will probably never happen.
Disagree that full type safety would require a lot of runtime checks. That's not the experience I've had in ReasonML.
Why would this be the case?
Many type-safe languages throw an exception if you attempt to access an element in an array that is out of bounds but javascript just returns undefined.
Typescript ignores the fact that accessing an arbitrary element of an array could return undefined. If it did not ignore this fact then it would force you to deal with the potential undefined value retrieved from any array access (i.e you'd need to write a runtime check)
In some instances it makes sense to use a special NonEmptyList type that guarantees there is always at least one element, so you don't need to check.
Oh, and build speeds so fast that it's blink-and-you-miss-it. I think that's a pretty important part of useful feedback (and one that I hear TypeScript is not that great at).
TypeScript has a dog of a compiler, I'll give you that. But that wasn't what I was referring to.
- https://reasonml.github.io/docs/en/editor-plugins
- https://reasonml.github.io/docs/en/faq#why-are-bucklescript-...
- Oh, and a really fun one from TypeScript: https://news.ycombinator.com/item?id=20045066
It helps sticking mostly to interfaces as it helps managing expectations. Class information gets discarded during compilation and that brings with it some quirks. Java Generics is another example where type erasure leads to quirky surprises.
If you spend a lot of time with a technology, day in and day out, you get intimately familiar with all of its shortcomings, and sometimes you maybe lose some perspective.
Having used Rust (enough to see its innovations and promise) and TypeScript (daily), I think they are incredible languages and platforms. Sure, they have shortcomings, but I think it's important to keep in perspective the huge advances in the state of the art they have brought us for these trade offs.
One thing I love about typescript is they care more for the actual JS/library ecosystem as-is, and don't design a language for an ecosystem that should-be.
On that note, our new Redux Toolkit package is written in TS, and designed to work great in TS apps with a minimal amount of type declarations needed. Really just declare the type of the action in the reducer, and get everything else for free. The new React-Redux hooks API is also a lot easier to use with TS as well.
I showed how to use both of those in the Redux Toolkit "Advanced Tutorial" docs page:
https://redux-toolkit.js.org/tutorials/advanced-tutorial
On a related note, I also recently put up a long blog post detailing my own journey learning and using TS, as both a lib maintainer and an app developer, with my takeaways on the pros and cons of using TS:
https://blog.isquaredsoftware.com/2019/11/blogged-answers-le...
The value comes when you consider what happens if a reducer (or saga or whatever) is updated in a breaking way (or removed wholesale).
Example: consider a scenario where you use `connected-react-router`, your codebase fills up with history actions being dispatched, then one day you remove `connected-react-router`, or its major version is updated and introduces some breaking change.
If you have a union type that includes all your own actions plus the `LocationChangeAction` from connected-react-router (and you use is consistently - e.g. your components take `Dispatch<YourAction>`) then you'll learn about the breaking change when your app fails to compile.
The alternative is you catch it later than compile-time, at worst _much_ later.
You might decide that the overhead in this scenario isn't worth it, and that's your call to make (I believe it is worth it) but hopefully this gives an idea of the point.
That being said, I still think TypeScript's solution to handling them of using "type guards" where the type of a variable changes in different scopes is definitely designed to match common JS patterns at the cost of added complexity. Most other languages I know of only have a single type for a variable (and if you want to pattern match you must give each case a new variable name).
This may be pedantic, but with subtyping many OO languages can give many different distinct types for a variable. In Java you can assign any non-primitive typed expression to a variable of type Object. So almost any expression in Java can be typed as Object.
What you are describing as novel is rather the phenomenon of type refinement in a pattern match or conditional expression. When an expression undergoes pattern matching, its type becomes increasingly refined. This is a useful feature in intermediate-to-advanced Haskell (known as GADT) as well as dependently typed languages.
Sure, but if you do this in Java you must use different variable names for the reference of type Animal and the reference of type Object.
I think algebraic data types and type refinement are great, but I’m not a fan of automagically changing the types of variables in different scopes if it can’t be applied consistently.
FWIW, Go also changes the variable's type in type-switch cases: https://tour.golang.org/methods/16
Personally it doesn't feel complicated to use/understand (at least the kind of thing that these languages do)
I'm still kinda new to the idea of using conditional branches to inform static type checkers but it sounds like it's an idea that has been thought about in some depth [1][2][3] (i.e. doesn't sound like it was jimmy-rigged to match common JS patterns).
I personally love type refinement. Hacklang's type refinement was the first time it clicked that statically typed languages could _actually help_ you write code instead of get in the way.
[0] https://docs.hhvm.com/hack/types/type-refinement
[1] https://sites.cs.ucsb.edu/~benh/research/papers/kashyap13typ...
[2] Type refinements (page 23) https://drops.dagstuhl.de/opus/volltexte/2018/9219/pdf/LIPIc...
[3] Occurrence Typing (section 8.5) http://soft.vub.ac.be/Publications/2019/vub-soft-phd-19-02.p...
Your server side code should have runtime validation though. You can't and shouldn't rely on TypeScript there for security reasons.
Client (JSON) messages will pretty much always start out as any, and you can't assume they fit your union any way shape or form. Taking the Cat|Dog example from the article, a client may pass {"kind":"cat", "bark": "woof"} and if you just go blindly from there to the union and then try to narrow that down, you've got yourself a problem.
You should use a library like JOI (or type guards with custom validation) to first make sure it fits your union type, then you can go wild with discriminated unions.
I always wanted a library that could perform runtime validation given a typescript type, but so far typescript doesn't expose enough type information in decorators for this.
Not really, unless you define any as union of every possible type. Also TypeScript won't realize you have narrowed the type (because it doesn't use such a definition) with all your checking unless you cheat with type guards (which are really just a prettier cast with optional user-created validation).
Then there's also the fact that thanks to getters your narrowing from any may be incorrect, which is probably why you can only narrow to basic types or with an instanceof operator, neither of which will help you with your JSON input.
tl;dr: Discriminated unions won't help you when you're starting out from any, because any is not an union.
I suppose I'm saying that the validation function itself is the precursor to discriminated unions being useful. <any> run through a type predicate function or an assertion type function (https://www.typescriptlang.org/docs/handbook/release-notes/t...) produces a typed object that conforms to your requirements for the type.
I definitely do wish there were some way to create these assertions/validators from the types automatically, but I think that might be impossible.
Apologies for any formatting issues, on mobile:
``` foo:any; isAnimal(foo); if(isDog(foo)){ // Dog } else { // Cat } }
function isAnimal(thing: any): asserts thing is Animal { if (thing.kind !== "dog" && thing.kind !== "cat && etc){ throw new AssertionError("Not an animal!"); } ```
// ...etc, except that string literals probably exist in an Array or similar, ie types that can be progressively derived / enhanced from the runtime code.
The more I use typescript I realize I want a more sound and stricter language. Like no prototype overriding at runtime.
There are many such compile-to-js languages but none nearly as popular as Typescript is.
I have like 30 different APIs I implement at work, but I only pull out a few attributes from each. I don’t want to go to the moon and back creating types for all this crap, I’ll have more than a 100 different types to model because of all the nested data and convoluted structures the APIs return.
I want the flexibility to incur tech debt where I want to, I don’t want the language to get in my way. That’s what makes JavaScript great!
The real benefit of using a well typed language here is that you only need to parse the data once at this point, after that the type system makes sure the data is of correct format everywhere else in your codebase.
It is a very good library if you control both sides of the communication, and have fit for purpose interfaces. Otherwise, you are better with any lower level library.
Besides, what is it with the strictness of Haskell JSON libraries? There's nothing that will even accept Window's BOM.
With Aeson, You always have the option work with a map directly and pull out properties by name.
That is also especially useful when you can't predict what keys or shape the data will have
But it's a very complex library. It may not show at your code interface, but that complexity is there at the documentation and type errors. It is also famous for generating bad runtime messages. There is even another library for fixing this, at the cost of even more complexity.
If you fall outside of its optimum usage scenario, it won't afford you more functionality than a low level parser, so you are much better saving that complexity and going with the parser.
OP was complaining that it's hard to type some kinds of payloads.
Sure you could write a custom parser for it.
With fromJSON in fact this is what you write. Maybe if you've only been driving Json instances generically, you never actually written a fromJSON instance in Aeson? I only use generic deriving in the simplest cases.. otherwise just write the fromJSON instance parser
My point was that you can defer parsing up front and just return a map of you don't want deal with the shape.
But sure you could write a custom parser as well, that falls more into the typed philosophy though than what js folks are used to
Of course a code review can catch it. Of course careful programming can as well. But those are external constraints.
Having internal constraints greatly reduces the amount of mess one can produce.
Admittedly, sometimes to a point to which you’ve typed yourself in to a corner.
I think TypeScript is essentially for retards. It is like "we need to lock everything down so the mediocre people we have to hire on mass can't break anything".
Come on, Typescript is more modern than C#. It's just started to being bloated a little bit since there are advanced type operators against structural typings. Or if you have to use a modern example, languages like Scala or Rust are more like it.
Also guaranteed compile + runtime is not what C# is. Probably Haskell is more closer.
Those are the things that keep people going to TypeScript.
The irony with the Javascript ecosystem is that most of it is now written in typescript or other languages. That includes mainstream frameworks, tools, etc. Basically anything that matters to large numbers of developers. And of course for frontend development people don't actually ship a whole lot of third party dependencies in any case (for code size reasons). So our need for interoperability with all of that is actually pretty low. Most of the dependency hell that is NPM is really about pulling in layers of tooling that fix and work around each other in very odd and convoluted ways (i.e. webpack). React is a comparatively tiny framework of which several compatible even smaller versions exist that people tend to use when they don't care about supporting obsolete versions of javascript. What it does is pretty clever but you can recreate it in your language of choice with not too much effort (and people do this for lots of different languages). There are react inspired frameworks for Kotlin, Rust, and probably Swift, C# and a few other languages.
Typescript is pretty neat if you are coming from the giant Stockholm syndrome in our industry that is Javascript. It's held us captive by being the only choice for browser based development. Thankfully that wasted 2 decades of captivity is coming to an end now. So we can get back on track improving our tooling, languages, and practices. Things like refactoring and code completion are not exactly science fiction (both worked great in the nineties already). I think it's wonderful that e.g. VS Code allowed JS/TS developers to join this millennium two decades late but it's hardly the final answer in developer productivity. Time to move on.
IMHO, using Javascript these days is something to avoid. It's a compilation target at best and as such a stopgap solution until we can target WASM instead, which is possible now, will become very practical soon, and a mainstream/defacto thing to do soon after (2-3 years?). Most of this is pending on some ongoing work on fleshing out features for threading, memory management, apis, etc. Languages like typescript have a transitional role in the brief period of time that remains where transpiling to and interoperating with javascript is still better/easier/more convienient than compiling to WASM. Beyond that, unless you have a lot of javascript legacy that needs to be preserved/worked around, there are probably other languages that you might prefer to use instead. And as I argue above, there isn't actually that much code to rewrite if you consider what little of the NPM ecosystem actually ends up in your application on browsers. A few thousand lines of React code don't justify being held captive by that ecosystem. Also, most frontend code doesn't actually survive its first birthday in any case. So, what legacy?
But luckily with OCaml/Reason, there is a modern alternative to all these bloated C#/Java like type-systems.
const shasta = new Cat("Maine Coon")
printDog(shasta)
> Dog:Maine Coon
I see a huge red flag. Coming from C/C++ it looks like a joke. Why are all these front-end static type fanatics not more interested in Dart, Purescript, Elm, etc..?As someone that's done c/c++/c#/lua/js/ts I've found typescript to be really awesome because it differentiates the types from the objects. Which has given me a much better appreciation of typing rather than the traditional OOP everything.
type theThing = { thing1: string, thing2: string };
and the actual object: const thing:Thing = { thing: 'hello', thing2:'more hello' };
"Thing" is not an object, I can't do Thing.new() etc. If I use a class in typescript it behaves just like you would expect C# / Java to.
This allows some really fun stuff with unions where I could do something like
const operateOnThings = (thing: Thing | Thing1 | Thing2) => { //do something based on what kind of thing it is }
Or I can build other types off of it like : type newThing = Pick<Thing, 'thing1'> & { newerThing: number};
Which is something I would have to do a lot of work with interfaces with in order to implement in C#. (I haven't done C# in a few years but I do love the hell out of it).
Having said that, I absolutely hate the ending of this blog post. I see blog writers do this all the time. He's just given three detailed, concrete negatives about Typescript, given enough information to infer that these quirks could be problematic for beginners and cost your team time and resources. Then at the end, he suggests that you use Typescript and waves it all away with some ambiguous bullshit about how static typing saves you a bunch of time.
There's so many of these claims that are just taken as common knowledge/best practices, that everyone treats as absolute truth that overrules everything else, even good concrete examples. I've been building entire apps myself for over a decade now, using both plain and typed (flow) JS, and every time I've seen somebody try and elaborate on how static typing saves you all this time, it's some pissant todo list bug that I would have solved in 5 seconds with plain JS.
Static typing is not that much about preventing bugs but more about forcing the code to operate over a well defined model.
Can you write good code without static typing? Yes. Can you scale and maintain it in a large organization, probably not.
It’s the same reason that there is any interest at all in MyPy for Python, Sorbet for Ruby etc.
In something like Java, it's the opposite: you get nominal types by default and you need to opt in to structural types (via interfaces).
Because TS is built on a duck typed language, tons of existing libraries would break if they had to declare explicit nominal types as arguments. Or you'd need to add custom interfaces for each library you use. Structural typing is what you want 99% of the time.
Remember that this all compiles down to JavaScript where it's all dynamic/weak types. Someone coming from C/C++ should definitely appreciate TypeScript over just plain JavaScript.
I.e. it would be quickly caught in a review, and the offending developer would have to bring cake the next day.
Yes, TypeScript is not perfect, but we take what we can get
Dumb example: imagine you had
Animal = Dog | Cat
Person = Plumber | Driver
If you want a list of “stuff” in your system , in typescript you just say you have Person | Animal.
In ML languages you would need to introduce an Either type so you’re working with Either Person Animal.
So now in your code you’ll need to introduce a bunch of Lefts and Rights. And if you end up needing to compose you’re introducing even another layer of Lefts and Rights. And you don’t get much of any or the inference capabilities of TS to just do this for you.
ML languages don’t have first class unions, they have ADTs, which introduce difficulties and lead to classic type safety weirdness like “why can’t I write a function that only accepts one ADT variant??”
Typescript resolves a lot of these, do you end up with a much better local maximum for typechecking IMO.
Typescripts type unsoundness means you unfortunately can’t really implement return type polymorphism though...
Indeed, your examples of the TS syntax are more convenient than ML, but that is reversed when going the other way, and decomposing the types into smaller sums instead of composing.
Because those are a massive paradigm shift. TS is easy. You can sell it as “JS but better. If all else fails you can always resort to any”. Selling Elm to someone who only ever used C# is very hard. I’ve tried.
It is also familiar enough for the hordes and horses of C# and Java devs in your company. It’s even an MS product. Enterprise yay!
A safe choice. You won’t be crucified if it goes wrong.
It’s sad, I know.
What seems like a good reason for accepting language design tradeoffs on the other hand is the package ecosystem. A language that doesn’t work seamlessly with existing third party js is probably doomed. It’s not too difficult to write a tiny language that is nice and consistent and compiles to js, but few will use it if you need ffi to interact with js code. The impressive engineering effort in TS is all about these tradeoffs.
This is why I’m having a hard time loving TypeScript, it’s a language for solving a real world problem (bringing types to the js ecosystem) in a pragmatic way, and that rubs me the wrong way. It acknowledges that js, js developers, and js packages are not going to going away any time soon.
Let's say I have a simple interface with a few fields. I want to make sure incoming JSON conforms to that interface.
I realize at runtime the interface isn't there, but is there some tool I can use to compile a utility to check JSON against an interface without having to write a duplicative JSONSchema or something else?
Because of this I prefer runtypes [1], because it's much more simple to get my desired behavior. My only gripe is that errors aren't all that descriptive.
I guess I could write a function to pipe/fold into the type, throwing a descriptive error if something fails, but I like the simplicity of runtypes.
Edit: I just discovered io-ts ErrorReporter [2]. That's way better than my solution, and I'm considering switching now!
1: https://github.com/pelotom/runtypes
2: https://github.com/gcanti/io-ts/blob/master/src/ThrowReporte...
But once you have those set up, it's an easy breeze, and the benefits are enormous. Our helper-method with the hopefully self-explanatory name "validateOrThrow<T>(t:TypeGuardForT)" is essentially called at every point where there is some form of incoming external data, and the amount of debugging-headaches that have simply disappeared because of this is mind-blowing.
The problem is that a lot of the things you want to validate aren't easily expressible as typescript types (e.g., valid email address, make sure two fields are always the same length, etc). If json schema are more expressive you want to use that as your source to generate the typescript interface instead of the other way around.
Write your interface in json or text proto then build it to ts interfaces and runtime checks.
Typescript doesn't support macro currently, so code generation is the only way to programmatically create 'type-checked' code.
Some cli-tool and library I'm using (available on npm):
tsc-macro: https://github.com/beenotung/tsc-macro
gen-ts-type: https://github.com/beenotung/gen-ts-type
ts-type-check: https://github.com/beenotung/ts-type-check
[1] https://github.com/epoberezkin/ajv
[2] https://www.typescriptlang.org/docs/handbook/advanced-types....
https://github.com/gristlabs/ts-interface-builder
https://github.com/gristlabs/ts-interface-checker
Example: https://github.com/alangpierce/sucrase/pull/468/files
- the typescript interfaces: https://www.npmjs.com/package/json-schema-to-typescript
- the runtime checks with AJV
So you have no duplication and you're type safe.
> 1. Interfaces with excess properties
... in immediate objects, which will never be reused. This is nothing worse than golang emitting errors on every single unused variable. There are good reasons to behaviours like this, even though not always preferable.
> 2. Classes (nominal typing)
It seems like the author straight rejects the idea of structural typing itself, by calling the concept itself a "quirk". This is rather about preference, not right-and-wrong. If this point is to be true, one should also reject duck typing, many other popular dynamic languages, a large portion of software industry, scientific researches, etc. Good luck with that.
> 3. Discriminated Unions
While I do agree that it's a shortfall of TS type system, the example is simply unrealistic. Using composite values as type discriminator is a bad design. Plus, the compiler is not even allowing anything destructive nor bug-prone.
They are all justified, and the author acknowledges this, but nevertheless surprising.
1. https://github.com/microsoft/TypeScript/issues/20774
3. https://medium.com/@trukrs/type-safe-javascript-with-jsdoc-7...
The correct use of:
printDog({
breed: "Airedale",
age: 3
})
is to explicitly cast it: printDog({
breed: "Airedale",
age: 3
} as Dog)
[0] https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&p...[1] https://www.typescriptlang.org/docs/handbook/advanced-types....
const dog: Dog = {
breed: "Airedale",
age: 3
}
printDog(dog)
An IIFE is safe too: printDog(((): Dog => ({
breed: "Airedale",
age: 3
})())The statement ‘const d: Dog’ and the expression ‘d as Dog’ are equivalent w.r.t. type safety. The compiler will error in both cases the if the value is not a Dog.
interface Dog {
breed: string;
}
const dog = {} as Dog;
console.log(dog.breed);
Using `as` is very unsafe in TypeScript.I've collected more quirks of `as` in this playground: https://www.typescriptlang.org/play/index.html#code/PTAEAMEs...
e.g.
‘foo as unknown as Dog’
// No error here
type Animal = 'dog' | 'cat';
const notAnimal = 'couch' as Animal;
// No error here either
type Food = { isSpicy: boolean };
const empty = {} as Food;
https://www.typescriptlang.org/play/index.html#code/C4TwDgpg...JavaScript is consistent. Expressive languages such as JS are consistent in their permissiveness. It doesn't stop developers from doing irrational things like comparing objects with numbers just like TypeScript doesn't stop developers from writing all other kinds of flawed logic.
TypeScript basically only protects code from typos. The worst bugs I see in production systems are almost never caused by typos or by comparing incompatible types; usually they're caused by issues in the flow and manipulation of data; for example the same instance is being modified in two different parts of the code without the other part of the code being aware of it; or in the case of pure FP a function is using outdated instances because a change in the underlying data source did not propagate through correctly, etc...
TypeScript is to a software developer what a spell checker is to a book author; it's convenient but once you run the final product past your editor-in-chief (whose analogy in the software world are unit/integration tests), the spell checking didn't actually add any value; at best it saved your editor-in-chief some time. It has no bearing at all on whether or not you'll get a Pulitzer Prize or a top ranking on Amazon.
The downside to TypeScript is compile time and this is a significant drawback IMO because it slows down iteration time significantly. Time spent waiting for the build to finish is time that was not used to add new tests and new features.
If you add up all the time that all developers on a project spent on waiting for the build (or lost their focus/train of thought as a result of waiting for the build to complete), how many extra unit or integration tests could have been written using that lost time? I think if you do the math, you will find that TypeScript is a net liability to the project.
It doesn't prevent asynchronous control flow issues like for example;
- Having two different parts of the code simultaneously (asynchronously) mutating the same object and causing data inconsistencies.
- Your code starts a new asynchronous job (e.g. setInterval) in the background without killing the previous/existing job which was launched earlier.
- An event listener was registered for the same event multiple times (e.g. from inside another event handler) without unregistering the previous listener and so every event now triggers multiple updates instead of one (and CPU usage keeps going up and you have a memory leak).
- It doesn't tell you when you forgot to trigger a specific event
- Or guarantee that two different parts of your code never run in parallel (asynchronously) when mutating some state.
- Or that you missed an edge case and forgot to update a specific instance's property
- Or that you were modifying the original instance of the object instead of merely a copy/clone of it as you assumed (or the opposite was necessary and you made the reverse assumption)
Only a human brain can prevent those issues (and the list of such difficult issues is practically infinite; my list is a tiny sample) and those are the real issues in software development, not typos and auto-completion.
Unless you're a web developer and your job only involves writing static webpages, you are likely to encounter much more difficult problems in your career besides not having automatic method name completion or typo prevention mechanisms. These problems I described above are complex enough that they make all the problems solved by TypeScript seem negligible. TypeScript's compile time delay becomes a bottleneck when trying to debug and resolve real difficult problems.
Precisely. No matter how good a language is, you can always say "but it doesn't do xyz". If you want to replace the human brain by a language, then you're looking for AI, not a language.
I understand what you mean, and I agree that these things are lacking from TypeScript. And I would love them. TypeScript is slowly adding more and more things, mitigating issues one by one, which were previously dissed off by developers with "it doesn't even do xyz".
I have my job because my brain is smarter than TypeScript's compiler. And it does help me, because I know which things I don't have to think about anymore, based on how I write the code. So it does help me, because I think less about the set of problems which it DOES solve.
In the first case the behavior is quirky because they are passing an untyped object into a function. If the object is typed the behavior is consistent.
The second example is about classes. Classes are created as easy extension objects via inheritance and poly-instantiation. Those ideas are friendly concepts for many developers but they increase complexity so I intentionally avoid classes and use a custom ESLint rule to enforce such.
In the third case they can solve for complexity with a simple refactor. The two interfaces are structurally identical so they should be a single interface with either a union on property values (static literals) or by defining a property again a union of other interfaces called by reference.
If Typescript never existed, people got used to simple nominal typed languages like Java would never notice there are much more advance type system ahead, and what can it potentially bring us - the lambda cube, the dependent type, the Curry-Howard correspondence.
After everyone have a much better understanding in type and the complexity in Typescript, simpler and more powerful languages would take off.
Edit: The article says "magical hidden properties".
type CompanyID = string & {readonly brand: unique symbol}
type OrderID = string & {readonly brand: unique symbol}
type UserID = string & {readonly brand: unique symbol}
type ID = CompanyID | OrderID | UserID
function CompanyID(id: string) {
return id as CompanyID
}
...
TypeScript can be very confusing sometimes, and given the learning curve I’d caution someone trying to learn modern JS away from starting out with it. But in even small codebases it’s an amazing improvement in safety, especially for refactoring.[1] https://learning.oreilly.com/library/view/programming-typesc...
That's not good. A type system should be crystal clear, predictable and reliable.
> and given the learning curve
Types should not be a complex thing with a steep learning curve. If you know Assembler and C it's pretty clear what types are about. Added complexity to your codebase is what you should try to avoid at all times. We have a limited capacity of things we can think of at a time, developers should therefore focus on things that really matter. In my daily work I see highly complex piles of spaghetti in TS that are 'type safe' and easy to refactor..
Nominal types are really a compile time only thing: if you have two structs (or classes) with the exact same fields, I'd expect them to be stored the same way in memory. And as a result I'd expect any function to work on them just fine, as long as they find semantically valid data at the right offsets.
The purpose of TypeScript is to provide static types to JavaScript without just turning it into a compile target. TypeScript type system is only confusing because it's modeling real-life ultimately type-less JavaScript code. It's not it's own language with it's own rules -- it lives and dies as JavaScript.
> Types should not be a complex thing with a steep learning curve.
Types can be as simple or complex as needed. Assembler and C have very simple type systems with very few features and can only model very simple situations. Types in TS don't have to be complicated -- blame JavaScript programmers for their crazy designs.
You'll have class members with accessibility in their declaration, and real run-time encapsulation.
At runtime? You can't, it's JavaScript, everything is public (for now at least).
https://news.ycombinator.com/item?id=19568378
https://www.youtube.com/watch?v=csL8DLXGNlU
I posted these Anders Hejlsberg quotes, who co-designed TypeScript, C#, Delphi, Turbo Pascal, etc:
https://news.ycombinator.com/item?id=19568378
>"My favorite is always the billion dollar mistake of having null in the language. And since JavaScript has both null and undefined, it's the two billion dollar mistake." -Anders Hejlsberg
>"It is by far the most problematic part of language design. And it's a single value that -- ha ha ha ha -- that if only that wasn't there, imagine all the problems we wouldn't have, right? If type systems were designed that way. And some type systems are, and some type systems are getting there, but boy, trying to retrofit that on top of a type system that has null in the first place is quite an undertaking." -Anders Hejlsberg
>Andrew Hejlsberg:
>Maybe I'll just add, with language design, you know one of the things that's interesting, you look at all of us old geezers sitting up here, and we're proof positive that languages move slowly.
>A lot of people make the mistake of thinking that languages move at the same speed as hardware or all of the other technologies that we live with.
>But languages are much more like math and much more like the human brain, and they all have evolved slowly. And we're still programming in languages that were invented 50 years ago. All the the principles of functional programming were though of more than 50 years ago.
>I do think one of the things that is luckily happening is that, like as Larry says, everyone's borrowing from everyone, languages are becoming more multi-paradigm.
>I think it's wrong to talk about "Oh, I only like object oriented programming languages, or I only like imperative programming, or functional programming".
>It's important to look at where is the research, and where is the new thinking, and where are new paradigms that are interesting, and then try to incorporate them, but do so tastefully in a sense, and work them into whatever is there already.
>And I think we're all learning a lot from functional programming languages these days. I certainly feel like I am. Because a lot of interesting research has happened there. But functional programming is imperfect. And no one writes pure functional programs. I mean, because they don't exist.
>It's all about how can you tastefully sneak in mutation in ways that you can better reason about. As opposed to mutation and free threading for everyone. And that's like just a recipe for disaster.
And these Larry Wall and James Gosling and Guido van Rossum quotes:
>James Gosling wants to punch the "Real Men Use VI" people. "I think IDEs make language developers lazy." -Larry Wall
>"IDEs let me get a lot more done a lot faster. I mean I'm not -- I -- I -- I -- I -- I'm really not into proving my manhood. I'm into getting things done." -James Gosling
>"In the Java universe, pretty much everybody is really disciplined. It's kind of like mountain climbing. You don't dare get sloppy with your gear when you're mountain climbing, because it has a clear price." -James Gosling
>"I have a feature that I am sort of jealous of because it's appearing in more and more other languages: pattern matching. And I cannot come up with the right keyword, because all the interesting keywords are already very popular method names for other forms of pattern matching." -Guido van Rossum
Also:
https://news.ycombinator.com/item?id=19568860
DonHopkins 10 months ago [-]
>Anders Hejlsberg also made the point that types are documentation. Programming language design is user interface design because programmers are programming language users.
>"East Coast" MacLisp tended to solve problems at a linguistic level that you could hack with text editors like Emacs, while "West Cost" Interlisp-D tended to solve the same problems with tooling like WYSIWYG DWIM IDEs.
>But if you start with a well designed linguistically sound language (Perl, PHP and C++ need not apply), then your IDE doesn't need to waste so much of its energy and complexity and coherence on papering over problems and making up for the deficiencies of the programming language design. (Like debugging mish-mashes of C++ templates and macros in header files!)
1) Be type safe enough to guarantee practically bug-free code (at least with regard to types, you can always have logic bugs)
2) Actually achieve what I want to do in less than a day
3) Upgrade easily when a new version of the language/tooling is released
The only thing that's missing is indeed better nominal typing so that we can more easily discriminate between logically separate instances of strings and numbers. For objects it's actually good enough to qualify for "perfect".
What you won't be able to do is to actually ship a product easily, as the ecosystem is virtually non-existent. Documentation - virtually non-existent especially for actually creating working products.
Of course, there are exceptions to this (like the Facebook Messenger being written in ReasonML), but people tend to underestimate what kind of engineering effort went into that. If you actually want to do stuff by yourself, use TS.
About the point the article makes:
1) I remember when excess property check was introduces I was really happy! Because I remembered bug that would be caught with it. I had some interface with optional properties in common library and I renamed property name. Of course when I updated this library in another project I didn't get any compile errors because this was optional property and now I was just passing some extra property which was ignored. Excess property check would have found it because I was passing object literal.
Structural typing is great choice in Typescript because it's basically type-safe duck-typing which is used everywhere in Javascript projects. It's not uncommon to have one component that accept objects with some properties but this object contains many extra properties because it came from different component and these properties are used elsewhere. It's standard pattern in Javascript world. But when your function requires some object of given shape and you are creating this object right now using literal syntax, then it's really likely that you made a mistake. Why create extra properties for just created object when you clearly see they are additional? And I read that Typescript teams is very happy with this feature[0].
However, excess properties check is great but useful only on call side. I cannot say that my function doesn't accept extra parameters. I would really love to get Exact types[1]. Which would allow me to force not having extra parameter in passed object.
2) As mentioned in article, Flow decided to have classes nominally typed. In Typescript everything is structurally typed so it's actually more consistent ;) I really hope we'll get nominal typing and opaque types sometime in future. I read TS team was thinking a lot how to do this. I don't remember what is the current status of this.
3) For me it's more problematic that Typescript doesn't narrow types of an union when you do conditional check. The reason is that in general it is unsound (but I think it would not be if you had only union of Exact types, that's another reason why I would love to have Exact types!).
Example in article doesn't have this problem so I guess this could have been improved. Sound like minor issue for me but maybe author could create suggestion on github to improve it and allow discriminator to be nested?
[0] https://github.com/Microsoft/TypeScript/issues/12936#issueco... [1] https://github.com/Microsoft/TypeScript/issues/12936
Google failed with their new language, Microsoft is having some success. Google claimed "javascript was too slow, so we need Dart". Hmm, turns out to be complete bullshit and electron applications are now the standard way to create desktop applications. Microsoft is claiming "you need static typing", which is more bullshit.
First Microsoft embraced JavaScript, then they made TypeScript to extend it, and now there is talk of "hey why not just use c# with web assembly etc". "why not just make deno TypeScript only". "what if Chrome only ran TypeScript, would it be faster?".
With vscode->typescript->github, Microsoft is managing to gain control of the masses of mediocre developers, while the good programmers will always be elusive. A digital divide is getting bigger.