TypeScript 4.1
devblogs.microsoft.com
devblogs.microsoft.com
Editors give great autocomplete suggestions (usually covers it for properties and return values). Documenting with examples is often helpful. If all else fails, cast.
Without advanced types, code needs documentation to prevent invalidity. With advanced types, code gets logical guarantees, but it often requires the same documentation. It's not necessary for everyone on a team to have cutting edge knowledge when there's sensible documentation.
Types create a synergy with documentation. Provable(ish) guarantees with good documentation is the best of both worlds.
Part of it is just being able to write code that is extremely dynamic, yet still provides an amazing developed experience.
For example if an API change occurs on backend, the first step is to make the change to the request/response typings in the frontend...from there the source code will light up with all the errors you have to fix. I can just click through each one in VS Code and fix them, then re-run tests to validate...love it.
Much more effective.
The dynamic JS space uses things like JSON Schema because they provide documentation and enable runtime validation, in a similar way to libs like io-ts. But they also provide API/network boundary documentation, as well as enable automatically generating client libraries (regardless of the client’s stack).
There’s a good opportunity for a library with all of these value propositions:
- simple, declarative, composable definition interface
- runtime validation/decoding/encoding
- DRY static type generation
- DRY standards-based documentation generation
There are (to my knowledge) no tools available providing all of those in one package. But I’ve built one (it was built on top of io-ts), so I know it can be done. But it was proprietary, for an employer, so I can’t share it.
But! I learned a lot in the process, and I’m building a new one. Zero dependencies, fundamentally a different design/approach. But I’ll definitely be sharing it with HN when it’s ready.
Edit to add: another thing is that many of the tools that provide some subset of those goals (especially static types <-> JSON Schema tools) require is out of band code/file generation (eg supply JSON Schema file input, get .d.ts file output, or vice versa). IMO this is additional DX friction and more error prone than the runtime/compile time parity of the common TS libraries. So another goal for my project is that the default approach is to define one interface and have APIs available to access all the other representations in the same flow, with file generation a secondary concern if there’s a need/desire.
import { Record, Number, Static } from 'runtypes';
export const Thing = Record({
thing: Number
});
export type Thing = Static<Thing>;
Then you can import it like this: import { Thing } from './thing';
function (thing: Thing) {
if (Thing.guard(thing)) { console.log('we have a thing!'); }
}I mean one can't represent the all possibilities of JavaScript "types" without re-implementing JavaScript itself, only a subset. I wonder where Typescript will stop.
I'd like to see Typescript compiling to "native" as well.
You don't have to personally understand TS's more complicated features to benefit from using TS in your project. I hope they continue adding richness to the type system so I can eliminate more bugs from my code.
I'm not sure what you mean by "in the flow itself" exactly, but just yesterday a teammate of mine was using one of our libraries in a small project without TS. They first called getThing(), which might return null, and then passed the result to doSomethingWithThing(), which did not expect a null. That sounds like a "flow" problem to me, and TypeScript literally would've warned them of it.
(Since this particular combination of functions is likely to be common, we added a runtime check for people who aren't using TypeScript now.)
> Most devs using it have never seen vanilla js and are more java minded that prototype minded.
I think you might be underestimating the number of people who strongly dislike Java's type system but very much appreciate TypeScript. They're very different, and hence it's very much possible to like the latter for reasons that do not apply to the former, or to dislike the former for reasons that do not apply to the latter.
When i think of a flow issue it's more things like "i forgot to check if the user was authorised to perform doSoemthingWithThing()"
...
// if(isAuthorised(user)) {
doSomethingWithThing(thing);
// }
...
How would typing detect the missing authorisation check in the program flow?Surely this is a case where testing is the correct tool to use?
Guest | AuthenticatedUser
and isAuthorised is a type guard like:
function isAuthorised(user: User): user is AuthenticatedUser { return user.isLoggedIn /* or whatever */}
as long as doSomethingWithThing accepts just AuthenticatedUser, yes it can be caught. You can also use discriminated unions with implicit type refining from TS without type guards.
doSomethingWithThing(thing)
Doesn't know about user, why does it need to be polluted with the concept of an AuthenticatedUser type which it does nothing with?"doSomethingWithThing" probably does IO, and the changes need to be traced back somehow (e.g. a ModifiedBy column in a DB). You can solve this context problem through a DI container, or by direct parameter passing, but whatever you do, there will be a layer which can expose the requirements as types.
I'm not a know-it-all who'll dictate how you need to structure your program on HN, so what I stated above is just me thinking out loud, but let's say it's up to the programmer to make everything safe-r with the tools available, be it runtime checks or the type system.
I agree with you that this doesn’t really solve the flow problem though: someone still has to decide that the function only receives AuthenticatedUser and not Guest. That’s not a net gain, it’s just shuffling responsibility around, and it could be a net loss if the calling function does “a bunch of identical things for any user type” and then “this one extra thing if user is authenticated.” But I’m not a Typescript user or a big fan of complex type systems either.
If all the time spent writing all those types was spent debugging, would TS win in actual development time?
If you're writing good unit tests already, why are you running into so many type issues?
If you already have to spend time writing tests, why spend more writing types?
TS types aren't really documentation. For other devs to use your stuff, you need JS Docs to actually explain things. Why write all those types too?
If I'm writing a quick and dirty project, it feels like TS types help a little, but if I'm following best practices on an important project, TS seems like pointless ceremony that slows things even more.
I do have my intuition though. (One day I'm going to keep track of the mistakes TS catchers early for me in a week.)
So yes: I think the typing lowers the correlation between lines of code and bug count, and that that outweighs the bugs introduced by the additional lines. (It is similar in that regard to many unit tests, I'd say.)
As for unit tests: it's not that I write unit tests first and add types later. The types are written as I code, and they allow me to not write some of the unit tests I'd written otherwise. The upside is that they're easier to write and easier to keep aligned with the code than unit tests.
So yes, I still have to spend time writing tests, but less of it than without types, and I save more time than the writing of types costs me.
> TS types aren't really documentation. For other devs to use your stuff, you need JS Docs to actually explain things. Why write all those types too?
I don't understand what you mean by "write all those types too"? Yes, I still have to write documentation, but just iterating the types is not documentation?
I've read that paper in the past and it doesn't actually answer most of the questions I asked above. It only says that X bug existed in some prior git commit. Maybe the dev caught that bug and fixed it before the PR with the commit was merged. Maybe it was caught in code review. Maybe it was theoretically possible within the function, but not within the actual program's use of that function. Maybe the time spent catching those bugs was less than the time it would take to add types.
> they allow me to not write some of the unit tests I'd written otherwise
A `typeof` assertion or similar is hardly more work and continues to function once the TS types have been stripped. If you expect to interact with the outside world, then you must test against unexpected types. If not, then good docs are still better (see below). Meanwhile, in every real-world TS project I've worked on, you wind up with tons of "template soup" where devs spend tons of time trying to find out which variant makes the type checker happy (or just giving up and slipping in an `any` type)
> Yes, I still have to write documentation, but just iterating the types is not documentation?
I have a function that takes a string and returns a boolean. What does it do?
It's likely that I can pass it any string, but there's a strong possibility that the function can't handle any random string. Does that boolean mean it's a test, that something was successful, or something else? What about side effects?
By the time you're done documenting this, when someone glances at the docs, they'll probably not worry very much about the types because they'll be obvious. Why write up a bunch of complex types when a simple, human-readable doc string does types and so much more?
OK, well I still don't have the numbers, so we have nothing better than intuition to go on regarding whether it saves time/improves quality or not.
> A `typeof` assertion or similar is hardly more work and continues to function once the TS types have been stripped.
Yes, and if a typeof assertion is enough, than you don't need any additional syntax in TypeScript either. But a `typeof val === "object"` doesn't tell me a whole lot though.
> If you expect to interact with the outside world, then you must test against unexpected types.
Agreed. That said, with TypeScript, you only have to do it once, at the point where you interact with the outside world. Once I've verified that e.g. my API response contains all the properties I expect, then I can pass it on to any other function in my code safely. Whereas without TypeScript, I have to be aware at the points where I access those properties that the original source of that value might have been the outside world, and to explicitly verify that it looks as expected. (Or alternatively, I need to still verify the object at the boundary, but have to manually know what properties of it are accessed in the rest of my codebase.)
> Meanwhile, in every real-world TS project I've worked on, you wind up with tons of "template soup" where devs spend tons of time trying to find out which variant makes the type checker happy (or just giving up and slipping in an `any` type)
Yes, I've seen that happen to. I will not argue that you don't have to learn TypeScript, and that if you do not (want to) put in the effort (or are unable) to do that, it might be counter-productive. In fact, I advised another team in my company to move off of TypeScript for that very reason.
> By the time you're done documenting this, when someone glances at the docs, they'll probably not worry very much about the types because they'll be obvious. Why write up a bunch of complex types when a simple, human-readable doc string does types and so much more?
Yes, type annotations are not a replacement for documentation. They help your tooling help you. So the reason to write up a bunch of complex types (well, preferably simple types most of the time, of course) is that your tooling can help catch mistakes early - I'm not working off of documentation most of the time. I read it once, refer back to it every now and again, but more than that would be a massive waste of time. My memory is a major asset in being able to quickly type out a bunch of code, but my tooling helps me by removing the need to memorise some things.
This is where the famous 'compiler' steps in and catches your typos and errors with "types" at compile time.
> If you're writing good unit tests already
Compilers are not fragile. Unit tests are fragile. IMO, a significant factor contributing to relation between N (lines of code) and B (expected number of bugs) is human error. Humans write unit-tests. For a fun spin, consider the fact that "more unit-tests" means "more lines of code" and thus "more bugs", but now in your "test suite".
[p.s. fragility above refers to the inevitable drifts between the original test-subject (and associated test code) and subsequent changes to the codebase that require updates to the test codebase.]
The worst bugs aren't types
* Syntax errors -- caught by JS runtime
* Math errors -- caught by unit tests
* Logic errors -- caught by unit tests
* Resource errors -- caught by profiling tools and unit tests
* Interface errors -- caught by integration tests, documentation, profiling and/or types
Of all the major types of programming errors, types only help with the least impactful one, the most obvious one, and the one for which there are other tools available.
In the end, the dynamic vs static typing argument has likely been going on since before most HN user's were born (since at least the 50s). I suspect we aren't going to reach a definite conclusion today either.
Sure thing, but wanted to insert a few facts informing your assertive OP regarding "lines of code" and "unit-tests".
p.s. "worst bugs"
The worst bugs in my 3 decades career have involved reliance on a broken test-base "verifying" widely shared code in an evolving codebase.
This is a major point: "caught by JS runtime" means you're only able to see it when you actually run your application and exercise that code path - or worse, that your user does so. Where the alternative is your editor literally indicating the error the moment you write it, the fix costing you so little time that you can fix them practically subconsciously.
>If you already have to spend time writing tests, why spend more writing types?
Because unit tests and types solve different problems and if you use one as a substitute for the other, you're doing it wrong.
Types exponentially reduce the set of possible inputs a function can receive, thus greatly reducing the number of tests that need to be written to achieve the same level of safety. Types are also much more precise and less brittle than tests, they establish clear contracts that reflect the actual structure of the program whereas tests provide assurance that some minimum subset of behavior is correct enough for the software to meet the expectations of users.
Ultimately, a lack of types is a form of technical debt because the data structures do exist whether you spend the time to formally acknowledge them or not.
1: http://earlbarr.com/publications/typestudy.pdf 2: https://www.reddit.com/r/typescript/comments/aofcik/38_of_bu...
It's not only your stereotypical Java developer who has to maintain large code-bases. There are much more monolithic code-bases than there are micro-services. Also, probably you have a much better memory than me, but I, like many others have trouble working with random objects with no contracts.
Additionally, try using a library like io-ts and you also get validation against types.
Typescript made me love JavaScript again.
But my gripe with Javascript has always been, that you often don't get any direct errors at all but instead end up with unexpected values in a different place that you then have to tediously backtrack.
A contrived example to illustrate what I'm referring to:
some_obj = {day: 2, ...}
next_day = some_obj.daz + 1 // typo in attribute gives you `undefined`, adding 1 to it results in `NaN`, not a number.
date = Date(2020, 11, next_day) // returns a`Invalid Date` object
console.log('date: '+ date) // outputs "date: Invalid Date"
In Python, every line after the first would raise a runtime exception and is thus in my opinion much easier to debug.What I particularly like about Typescript is that I can use as much or as little of it as I deem necessary. E.g. I start off a prototype using the `any` type a lot, and only in future iterations, I start tightening the definitions as the requirements for the project become clearer. So far the overhead has been quite minimal.
The one downside so far is, since I learned Typescript by just starting using it, without a deep dive in documentation, books, etc. trying to grok code, well especially type definitions of libraries (which I often prefer over reading documentation) that use the more advanced features has been quite a challenge.
I started working this way and it was fine, that's how I always worked anyway. Then I started leaning towards working the types first and the code later, and I've found it works better and better for me.
Much of today's programming is "stitching pieces together": pieces from the libraries you are using, from external services you use, from your platform's APIs, and then creating the few extra pieces that are unique to your puzzle.
When you code first, you are directly painting pieces while trying to picture how the final puzzle will look like in your head.
When you type first, you are just cutting the shapes of the missing pieces and already assembling the puzzle. Once this is done, you are left with a full picture that has some blank spots, and it becomes much easier to just paint those in.
I'd suggest skimming this page (a concise overview of the advanced features): https://www.typescriptlang.org/docs/handbook/advanced-types....
Also, reliable auto completion is very helpful not just for the method names but for properties etc.
Personally, I have no issue with TypeScript purely as a language and I concede that it provides much better IDE code completion than JavaScript. That said, I don't agree that its benefits offset the drawbacks of the transpilation step, the source mapping and the added versioning complexity between JS and TS.
I also miss the way that JavaScript encouraged people to define very simple function signatures (e.g. strings, numbers, plain objects/clones as arguments and return values).
For example, I really liked how the React/Redux community came up with a philosophy around cloning state objects before returning them from functions in order to prevent unexpected mutations later (essentially force everything to be pass-by-value). I think this philosophy does not translate very well to TypeScript which encourages developers to pass around complex live instances (which have their own methods) instead of raw objects and other primitive state representations.
As Alan Kay pointed out, OOP is not primarily about objects, "The big idea is messaging". Instances should communicate with each other via simple insterfaces using simple messages; they should avoid passing complex live instances to each other. If an instance has methods, that is a complex live instance and it shouldn't be used for messaging between different components.
I wish this were true, and maybe it was in some cases, but not broadly. Many "types" used in JavaScript libraries are horribly complex. For example, the type signature of jQuery's `$` function, or moment.js's main function, or `server.listen` in Node.js, etc.
In fact I think the opposite argument from yours could be made with a straight face: not having to declare types encourages more complexity. I don't really agree with that either, though; in my experience this is more a matter of developer discipline and API design skills.
One thing I'm certain of is that if libraries are going to have complex types, I'd much rather have them be explicit and machine-verifiable vs hidden/implicit/hope-the-developer-wrote-a-good-docstring.
- https://redux.js.org/recipes/usage-with-typescript
- https://react-redux.js.org/using-react-redux/static-typing
For example, in your second link, one of the functions expects a 'complex' instance DispatchProps which has a method toggleOn... This abstraction doesn't make sense conceptually. What is a DispatchProps? It doesn't adhere to Alan Kay's notion of a 'message', it's clearly a structure (it's a complex one because it exposes a method). Components should communicate to each other via messages, not structures.
Also, the method builder.addMatcher(...) accepts a function as an argument - It doesn't seem like an ideal abstraction either. Functions are not messages.
Also, the thunkSendMessage function signature is very complex; the return type is highly convoluted. That's definitely not a message.
Overall, I see a lot more of these complex instances being passed around in these examples than used to be the case with JS when mostly just raw objects were being passed as arguments and returned.
You can already see the complexity seeping into the interfaces. Just a few years of TypeScript is distorting the original philosophy. I remember Dan Abramov was very careful about what to pass into functions and what to return from them and made it a point to encourage cloning objects using the ... spread operator.
That philosophy appears to have been forgotten.
A lot of people seem to assume that "using TS" means "must use the `class` keyword and deep inheritance chains", ala Java and C#. Redux, on the other hand, is FP-inspired. Nothing about the Redux core involves classes in any way - everything is just functions, including the middleware API, with an emphasis on immutability.
You brought up Redux's use of immutable state updates, but then said "that doesn't translate well to TS". I was attempting to show that it _does_ translate just fine to TS, because TS lets you write functions and make immutable updates. `return {...state, field: value}` works as fine in TS as it does in JS. I wasn't trying to touch anything about "what a message is".
Having said that, the rest of your observations about the Redux core and React-Redux APIs in this comment show a general misunderstanding of what Redux is and how it gets used. After all, Dan and Andrew came up with the actual React-Redux API, the concept of `mapDispatch` for passing action creators as props to React components, and the thunk middleware. The Redux Toolkit `builder.addMatcher` API is a recent addition, but all it is is syntax sugar for "if the dispatched action is any one of these types, we want to update to it", same as if I wrote a multi-condition `if` statement by hand.
None those have anything to do with TS, and they did not become more complex because we're now using TypeScript. In fact, it's the other way around - the complexity of the dynamic JavaScript behavior actually requires us to write much more complex TS types to capture how the code actually works. (There's a good reason why the React-Redux TS types are insanely complex, and I'm so glad they're maintained in DefinitelyTyped instead of by us! `connect` has so many overloads and different options that affect downstream props values, it's almost impossible to capture that with static types.)
Complex interfaces lead to 'tight coupling'; developers have known for decades that this is bad but we don't seem to be discussing it much anymore. The reason why JSON-based REST APIs became so successful is because it greatly reduced interface complexity (compared to XML-based SOAP) and in doing so, it loosened the coupling between different services.
Until we start discussing interface complexity, nobody will fully realize what the drawbacks of TypeScript are. My main problem with TypeScript is that it is most useful when code quality is low (high interface complexity; tight coupling). That's what I mean when I say that it encourages bad programming practices; the people who find TypeScript most useful are those who tend to produce the worst code in terms of interface complexity.
the `mapDispatchToProps` idea has been in `react-redux` since the very beginning. I don't understand what are you complaining about here. If you prefer, you can ignore this practice and pass the whole dispatch function to the component, and then call `dispatch(actionCreator(params))` directly. Typescript won't get in the way of you doing that.
> Also, the method builder.addMatcher(...) accepts a function as an argument - It doesn't seem like an ideal abstraction either. Functions are not messages.
The addMatcher API is not concerned with sending messages, it is concerned with receiving them. You are specifying what should happen when a certain message type arrives. Hence it seems normal to me to specify that as a function that given the message does some stuff. Since these are reducers, the "does some stuff" part is: given a previous state and an action of type X, compute and return the new state Y.
> Also, the thunkSendMessage function signature is very complex; the return type is highly convoluted. That's definitely not a message.
Of course not. It is a function that sends some kind of message after asynchronously calling some API and getting its response, or another kind of message if the API call failed. That's what the complex type says. The usefulness of thunks is that you do have this notion of "ongoing asynchronous process that may succeed or fail" explicitly represented (so you can for instance cancel it), which you'll have a much harder time if you want to represent by using just actions and reducers.
> You can already see the complexity seeping into the interfaces. Just a few years of TypeScript is distorting the original philosophy. I remember Dan Abramov was very careful about what to pass into functions and what to return from them and made it a point to encourage cloning objects using the ... spread operator.
I think you are misremembering some things here. Dan Abramov used to advocate for a strict separation between presentational and container components [1], including the use of `connect` in exactly the same fashion you are criticizing in your first point.
Likewise, cloning objects using the spread operator was never a goal in and of itself. Dan advocated for it as an easier way to avoid mutating the state. Not mutating the state was the point here. Redux-toolkit accomplishes this point by using immer, a library that conceptually gives you a copy of the state so you can just mutate and return that instead (with the advantage that non-modified parts are not cloned).
Also, you can just not use that and return spread-operator-cloned objects from your reducer functions instead. Everything will keep working normally (and passing the type checks) in that case.
[1] https://medium.com/@dan_abramov/smart-and-dumb-components-7c...
PS: Thanks for your work Mark!
Some of the bugs exposed were the bugs in the 'flow'.
But when testing properly, these should anyways be found later. Or am I missing something?
Again, I think there's a difference between bug and a coding error. A bug is a mismatch between a software's function and the user's expectation.
And a typed-inferred error during compile time is just a hint that some code is incorrect. It may lead to a bug, but at the point of compilation, it's not a bug.
I'd like to point out that if you agree that typescript prevents stuff that may lead to bugs, without getting into the semantics of what a bug is, you may agree that it's useful.
Someone else will be changing that code later. Knowing (for example) that the parameter is a string will be zero help in knowing what is being done with that string. It also won't help know what edge cases the code was handling.
Also, types give a false sense of security. A new project was integrated into an old website (one with legacy dependencies, but doing millions in transactions every day, so unchangeable). It broke and they couldn't figure out why. They had the TS types for functions, but an old framework (prototype or moo iirc) overrode js built-ins with incompatible versions. Later they got bitten again when that code changed the object types. If they'd been writing js, they would have written dynamic checks from the start, but it's easy to forget that once you compile, it's just js.
It can prove to be false because many projects don't have sufficiently good code coverage.
Of course, it's also possible that there simply are no unit tests at all.
In those circumstances, TypeScript might just result in less bugs overall. While one could argue that unit testing should be commonplace, reality doesn't always live up to that standard.
The conpiler being silent, here is only an indication that your types and code match. Not that your assumptions about the program match with its outputs (e.g. the user interface).
However, unit tests are usually written to assert assumptions. Or to "freeze" certain parts of the code so that these mismatches don't happen.
In contrast: Types within the source code and depending on your way of thinking about types, many programmers will not see them as "declarative unit tests".
In practice, this means that you sometimes get "surprised" by some unit tests that are failing after a refactor. That's good because it sheds light where you've made mistakes when changing your code.
To some degree, of course, this is true for types. E.g. they will always help you to point out when two APIs mismatch. However, a test usually is contained within a unit with a clear description motivating its existence. It's so much harder to accidenitally changing a test for the worse than it is to change a typed function signature for the worse.
Lastly, very often functions are crucially dependent on input values and not their types. So even if, in a dynamic language, you get b=0 input into div(a, b) return a/b and it's a valid type, you should test for values as in this case as you can't divide by 0.
So in many cases, even with inputs it'd be necessary to unit test e.g. function signatures etc..
As you say, types are a more declarative (rather than procedural) way of asserting assumptions.
>In practice, this means that you sometimes get "surprised" by some unit tests that are failing after a refactor. That's good because it sheds light where you've made mistakes when changing your code.
>To some degree, of course, this is true for types. E.g. they will always help you to point out when two APIs mismatch. However, a test usually is contained within a unit with a clear description motivating its existence. It's so much harder to accidenitally changing a test for the worse than it is to change a typed function signature for the worse.
I'd argue it's just as true for types as it is for unit tests, if not more so. You can get "surprised" by the compiler when refactoring methods in just the same way. I'd argue you get more information with types, because it hooks into the LSP and identifies everywhere in your code that now fails. In contrast, a unit test only tells you that unit test failed. It is still up to you to find the actual locations in the code. In this way, unit tests can be thought of as a parallel program. This isn't true of types, which are directly embedded into the program. Put concretely, if you removed a property on a type then the LSP and tsc would tell you every single place that property is missing.
That's the crux of my argument. Types have better tooling, which helps with both the "delclarative unit test" part and equally importantly with refactoring. You can check for more things with types, like whether your `switch` statement exhaustively goes over every option or is missing any. Your tools also understand the types (like the LSP), which helps with refactoring.
I'd also argue that types are usually easier to understand than tests. While good unit tests can provide good examples on how to use an API, types exhaustively tell you what a thing is and what it is capable of.
>Lastly, very often functions are crucially dependent on input values and not their types. So even if, in a dynamic language, you get b=0 input into div(a, b) return a/b and it's a valid type, you should test for values as in this case as you can't divide by 0.
That's a great point. There's absolutely still a place for tests. Types are not a like-for-like replacement, and each have their strengths, but there is significant overlap. If one wanted to exhaustively enumerate in options in a `switch` statement, and ensure division by 0 issues, then they would need both.
It's great that Typescript is catching on, and we're starting to adopt it in my team, but I hope the TS people don't think they can keep adding new features to it in perpetuity, otherwise it's going to turn into C++.
In other words, typescript doesn't make the language any bigger. If you're struggling to understand typescript code you could just ignore the types, and you're back with regular JS. This is different than the issue some people have with a big language like C++, where the sheer number of features can make it hard to get a grip on unfamiliar codebases.
I have to deal with this sort of code on a daily basis. When I talk to the people who wrote that code, they mostly shrug and say that C++ was too complicated for them, they just used what they know.
The flip side of this is people (like me) who know C++ pretty well, keep track of most of the advanced features, and use them semi-regularly. Unfortunately, this effectively creates a barrier to entry to those same other devs. You can blame those other devs all you want for not keeping track of the language, but what if they don’t want to and have other, more important thing in their lives than to keep track of the language? So I’ve come to the conclusion that in my opinion the problem is you (or in that case, me), not them. If you want your code to be long lived and maintainable you have to take the people around you into account. I’ve come to consciously limit the number of C++ features I use in code other people see to a bare minimum.
But the basic premise holds, I hope.
The more advanced types just allow more specific type definitions. So if try to you them, and you can't get them to work... you can just avoid the advanced features and leave the type definitions more vague. Just like how if you can't get the basic types to work, you can always use "any".
Furthermore, your new claim is also nonsensical. The fact that I don't have to use all the advanced features of my languages doesn't alter the complexity of the language.
Given that the type system is part of a language, I have no idea what you mean by trying to draw a big distinction between adding features to a language and a language's typesystem.
Stage 1: Put types into JavaScript.
Stage 2: TypeScript types are not expressive enough.
Stage 3: Put JavaScript into types.
I'm looking forward to stage 4 where the expressions inside TypeScript type annotations get so complex and hard to maintain that someone creates TypeTypeScript that lets you put type annotations inside the code inside type annotations.
Or set `noImplicitAny` to false if you want to retain some type checking.
...and all the older folks just keep pointing at lisp and shaking their heads.
Just don't turn on the `noImplicitAny` compiler option. Yes, that's right. It's the default. https://www.typescriptlang.org/tsconfig#noImplicitAny
Shake away, lisp folks. If you can show me how to run lisp in a browser with tolerable performance and debugging, I might understand why you're shaking your heads.
We need a way to go back to JS without hurting the egos of TS community. It will require a massive marketing innovation to achieve this.
Can you tell all the companies to stop obsessing about TypeScript and to allow developers to use JavaScript too? That would be awesome because now it's almost impossible to find a JavaScript job.
I'd rather get social security and live under a bridge than go back to TS. That would be a more productive use of my time.
I don't have that much influence. Most of those companies have come to their own conclusions.
At my company, the reason we wouldn't allow that is that it makes the codebase more difficult to approach for a new developer. Knowing the types accepted and returned is helpful for reading unfamiliar code.
https://github.com/capsadmin/nattlua
You can see some examples in the readme.
To me it makes sense that the typesystem is basically lua rather than a mathematical ish looking language.
It could prove to be more difficult to work with but I guess I'll find out.
It's not without precedent - Haskell has a Turing complete type system. (There's also Scala, Rust, and C++ templates which are all arguably Turing tarpits.)
I don't think my comment sounds cynical.
> You don't have to use features you believe are "so complicated".
One has to be fluent with reading type definitions thus have good understanding of the type syntax. I must admit, from time to time I could not make sense of them easily when reading declaration files from third parties.
If you ever do have to dig into a really complex type, that might be painful, but keep in mind that the reason we have more sophisticated types is so that we can maximize correctness and reduce the chance of bugs, the fact that you're getting a type error at all means whatever you wanted to do is definitionally unsafe, so it's good that typescript is empowered to tell you not to do this, even if you don't immediately understand why.
Yes you do, you need to be able to read complex type signatures in order to know what a function or a method accepts, and for that you need to understand Typescript's type syntax, it what it is all about at first place. In case you have a compiler error you need to understand how the type system works to make sense of an error. You can't deny that simple fact, that's the whole point of typescript.
Lots of people simply prefer to know at compile time that their assumptions were wrong than having to reverse-engineer how they were wrong from stack traces at runtime.
If you don't understand the type of input that's appropriate for a function, a lack of type annotations does not increase your understanding, instead, your incorrect assumptions are literally left unchecked to wreak havoc in live systems. Like I said, it's better that typescript stop you from doing the wrong thing even if you don't immediately understand why it's wrong, but when in doubt most people just check the relevant documentation, same as if they lacked type annotations.
I didn't miss your point, but maybe I didn't explain mine clearly enough. You don't have to use complex types, even with 3rd party libraries, just figure out what goes into the function the same way you would if there were no type annotations.
And that's not what I said. Stop trying to pick up a fight with me or something by misrepresenting what I've said just for the sake of hearing yourself taking, it reflects bad on you, especially you doubling down.
I only talked about the fact that there is a limit to the degree of complexity Typescript types until it itselfs turns into a javascript interpreter. your answers have absolutely nothing to do with that matter.
That makes sense when comparing it to other languages, but compared to JavaScript: those type definitions are a pure bonus? In other words, if they'd have been written in JavaScript you'd still have to make sure to pass in the correct data, but you'd have only the documentation to guide you there.
(Of course, if you yourself are using TypeScript, you do have to know how to make sure that TypeScript knows that you're passing in the correct data. But the flip side is that the more complex types will allow for better inference that helps your editor to provide better guidance on how to give it the correct data.)
TypeScript advanced types are there to model existing JavaScript codebases, but when designing software from scratch I've seen that is too tempting to go crazy with the types, ending with something pretty messy (JavaScript! :-).
> [A Static TypeScript] program is compiled to machine code in the browser and linked against a precompiled C++ runtime, producing an executable that is more efficient than the prevalent embedded interpreter approach, extending battery life and making it possible to run on devices with as little as 16 kB of RAM (such as the BBC micro:bit).
To give a commonly-encountered example, rather than the easily-introspectable options object whose schema is specified by a zero-runtime-cost TS interface, in a traditional OOP language like Java you would write an XOptions / XOptionsBuilder class to maintain type safety, which is at once more opaque, with less predictable behavior.
It's not a horrible abuse if you aren't trying to make the framework/library statically typed in the first place.
Idiomatic APIs for dynamically typed languages use the flexibility of dynamic types. Idiomatic APIs for statically typed languages use the structure of static types.
The problem comes when you try to put one into the other's world. It's like watching a fish flop around on land. There's nothing intrinsically wrong with fish locomotion, it's just not optimized for that environment.
Of course it is, because even if there's no type system the people using the library still have to make sense of the complexity. A function with a dozen, context-sensitive overloads that accepts data without a clear pattern isn't just hard to type, it's also hard to understand for a user. Even with dynamic typing you should be able to clearly state what a function does.
That's why, even if I don't plan on using Typescript itself for a project, I prefer libraries that have type declarations or are natively Typescript. It shows the library authors at least attempted to understand the type system they were creating.
Haha, great way to put it
Except all of it can be tested and validated at compile time so... not dynamic?
When some programmers favored dynamic languages over the statically typed others looked at them like at mad.
The thing is that dynamic languages fans just wanted to have semantics as rich as this one and were willing to sacrifice even type checking altogether for it because no statically typed language was willing to deliver this semantic in any reasonable manner.
[0]:https://github.com/DefinitelyTyped/DefinitelyTyped/blob/mast...
In years of using TS I have had to craft a few exotic generics, but have otherwise never ran into "really complex" types.
https://github.com/DefinitelyTyped/DefinitelyTyped/tree/mast... felt like to me as one popular example for what should be really simple in practice.
I thought you were talking about types in your own code. Lodash being a large, heavily overloaded and generic library it seems fair that the definitions are complex.
When it comes to external code like that, I don't think you can argue against the value of intellisense and safety (especially when upgrading versions). Those are in my opinion the biggest benefits of having strong types.
In your case, your alternative is back to untyped, unsafe JavaScript. Maybe that's not so much better. Another alternative would be to move to a language designed with types in mind from the get go, say OCaml (with ReScript) or Java (with JSweet), or others.
Bottom line, I think that claim is still true:
"Forcing types on a dynamic language like this seems to inevitably lead to really, really complex type definitions which just confuse things tremendously"
But if that is a good or bad trade off for you to make, I think that's very contextual, it depends. Maybe that's okay for someone, and they'll use TypeScript. Maybe that's not okay, and they'll go back to JavaScript. Maybe they'll instead explore a ground up typed lang like Java.
You end up with write only code that while typed is overly complicated.
The semantics of a number or a string aren't described by their basic type.
Yeah I'd love something that just compiles .ts to a binary. But I can already hear the cries of "embrace, extend extinguish" if that happened.
https://www.typescriptlang.org/play?ts=4.1.0-beta#code/PTAEB...
Something like -> "Hey! it looks like your function only accepts a fixed set of strings. Would you like to use String Literals?"
I don't only code in Typescript so it's not practical for me to keep up with every language I work with.
Search works great if you know what you want, then you'll end up on these excellent release notes or a stackoverflow post and off you go. The trouble is when you don't know what you don't know.
For example, with an opportunity for string literals, there will be a light bulb in the left border. Pressing "Ctrl ." will bring up a list of suggested fixes. You can opt to select one, hit Enter, and the changes are automatically applied.
I believe there are similar extensions for VS Code.
I've written millions of lines of C# and hundreds of thousands of TypeScript, and I use JetBrains JDEs for both these days. Class generation is absolutely not something I need or miss in TypeScript.
I have apps with thousands of lines of implementing comparison/equality interfaces, overloading operators, etc. for my data objects. You can definitely write it all in Notepad, but it would be a massive waste of time.
Review early and (fairly) often, lest you embark on implementing a FrobNibulatorFactory from the ground up only to be told there is one already in lib/util/frob/util/tools/misc/nibulation.
Not everyone has peers, alas. The imaginary ones help (rubber ducky, you’re the one...)
.NET's Roslyn compiler supports analyzers that you can write yourself which also support code fix providers to automatically refactor code. All analyzers and code fix providers are written in .NET languages (C#, F#, etc.), so the possibilities are endless.
The Rust compiler exposes a lot of information, so it's actually quite powerful in my experience.
[0] https://marketplace.visualstudio.com/items?itemName=ironcev....
At least the pain is a good indicator that there's something you're missing :) IRC has been somewhat useful for TS, albeit with mixed results compared to say, the gentle gods you will find in #postgresql.
Having worked with Clojure for 4 years, and a JS (ES6 and +) codebase, static typing is amazing.
There are far fewer bugs at runtime because of incorrectly setting or forgetting to set keys in a map or a property or accidentally passing the wrong value to the property. Yes, interop with JS is a little messy, because of the dynamic nature of JS, but a large class of errors can be significantly reduced by Typescript.
As for the argument , unit tests can catch most of the problems Typescript solves, my counter is that
It reduces the amount of tests you need, to check getter/setters,types etc SIGNIFICANTLY. You can focus your unit tests on the more functional aspects than having to worry about basic property access.
All high-level languages have a learning curve. I don't expect to pick up the nuances and all the little bits of Typescript in a matter of weeks or months.It might even take years, but I think Typescript and its experimentation with providing a well typed Frontend interface is amazing !
Exactly! unit testing a return type is such a backword step in programming
When you wrote/write Clojure, did you ever use spec? Any insights into how it compares with TS? They seem to give similar guarantees for this aspect, but I haven’t played with spec yet.
spec , IMHO, seemed oriented towards validating properties and values in maps, as Hickey mentions in his talk, it is all about manipulating data.
Spec is opt-in. unless you are working with code that has already been spec-ed , you will run into the same problem that TS + JS interop has.
I found spec a little too intrusive (especially the namespaced keywords) and sometimes I wasn't sure what it was meant for, TBH.
In my first company, it was a bunch of cowboy programmer that didn't really care about typing the codebases, too cool to write unit tests and simply wrote enough end-to-end tests cases to solve the problem. I was just starting out and had NO idea on what was good programming practice.
In the second firm , we had used schema to type the codebase partially and it worked. But the type checks weren't compile time , but rather during unit tests and when running with type-checking enabled (I think this is the same for spec as well)
I think the advantage of TS comes from the static types (+ checking). You get immediate feedback before you can even compile it into javascript.
Why is alignment accepting a color? It doesn’t matter how good the type checking is if you are creating the wrong type to begin with.
That aside, congrats on the release!
By any other name would smell as sweet.
So Romeo would, were he not Romeo call'd,
Retain that dear perfection which he owes
Without that title.
https://davidtimms.github.io/programming-languages/typescrip...
- more accurately and more maintainably typing existing JS interfaces
- enabling more advanced compile time features for libraries
I’m probably one of these “typstronauts” you reference, I have extensively used nearly every advanced type feature in library code. But I’ve seldom found a need for any of them in consuming code. My approach since I adopted TS has always been:
- libraries can and should provide types at whatever level of complexity necessary to make library usage flexible, safe and simple
- end product code should use simple types wherever possible and depend as much as possible on the flexibility, safety and simplicity of the interfaces provided by those more complex libraries
I hope I never have to use it but that its existence makes typed libraries more "correct".
e.g. get({ name: 'Toby' }, "name.prop1.prop2") would fail as you're trying to call .prop1 on a string.
Really blew my mind. I'm used to having to give up simple APIs like that when moving to a statically-typed language.
get({ a: { b: { c: ['foo'] } } }, a *> b *> c *> _0)
and a macro to unfold the lens from a literal string is probably something that exists as well.There are of course nice qualities of simple functional composition and custom operators, though it comes with another layer of overhead that you wouldn't necessarily always choose over a simpler API in every case. Richer typed expressiveness lets us pick the right trade-off without being forced.
It will be interesting to see if they add metaprogramming capabilities next (allowing anyone to create a class like Uppercase<T>), although I’m not holding my breath.
Edit: see this SQL made out of the type system: https://github.com/codemix/ts-sql
https://twitter.com/toolslive666/status/1309533781137780741/...
Agree with the general sentiment though.
I'm up to speed now, but I still feel I'm missing an opinion.
e.g. I have naturally been drawn to "maximally infer" types, whilst I see other developers would rather re-create/co-locate a type with its function/component.
Does anyone have a repo where they think the TS is near-perfect? Or even better, a deeply opinionated Style Guide?
I've been starting to build speed-focused TS type-checker that is intended to be used in single-file editors like VIM to highlight errors, because it starts checking from a single file and just traverses files necessary to check types within that one file. If you defined your types at a module boundary it can bail out super early, since it doesn't need to look at the rest of the code of that module at all.
Most linters have rules that will force you to explicitly declare types at module boundaries.
Other than that, do whatever you prefer.
- Ideally they should either not be derived types (using eg type parameters), or if they are they should be derived using types from the same layer of your project/service. This makes it much more likely you’ll notice if you introduce breaking changes.
- Types should also between network boundary layers and business logic, regardless of your module structure. This ensures your business logic’s interface is clear regardless of how it’s consumed, making it more easily testable and more portable (eg to other transport formats/protocols).
This seems... Wrong. Better leave a comment because other people might assume it's a leftover from a refactoring or such.
It's easy to get wrong since it overloads syntax (casting vs boolean ops) and is very hard to google (as many operators are).
https://stackoverflow.com/a/63867074
… for what is literally a one-liner elsewhere:
class User :ro {
has name => isa => Str;
has email => isa => Str->where('lc =~ /^[a-z.@]{1,32}$/');
}
User->new(name => 'Dave Morrison', email => 'Dave.Morrison@gmail.com');
User->new(name => 'Van Morrison', email => 'Van-Morrison@gmail.com');
User->new(name => 'Jim Morrison', email => 'Jim.Morrison.is.my.name.and.it.is.too.long@gmail.com'); export type Camel<T> = { [K in keyof T as K extends string ? `${Uncapitalize<string & K>}` : K]: T[K] extends Record<any, any> ? Camel<Exclude<T[K], null>> : Exclude<T[K], null> }In most other frameworks there seem to be some arcane incantations required to include typescript into the build system. I have not looked into this recently - is there a straightforward way of including Typescript into a Rails, Gatsby, Hugo, Laravel, Wordpress, etc. project?
I feel like Typescript popularity would explode if the above level of friction became near-zero.
I’m a fan of the Runtypes library for this use case. If you design your typescript interfaces using the library (admittedly a bit magical to me) you can get runtime errors if API data doesn’t match the expected type
But since it isn’t, augmentations may be an option. I’m not at my computer right now so I can’t verify that this would work, but you may be able to add an overload that returns `unknown`. Unfortunately with overloads, I’m pretty sure the built in types would be selected first without some additional specificity.
This feels like a good opportunity for a safer standard library package wrapping globals, DOM, Node built-ins, etc.
[1] https://www.typescriptlang.org/play?#code/PTAEEkDsDMFMCd6wCa...
https://github.com/microsoft/TypeScript/blob/master/lib/lib....
You could provide your own lib.d.ts with `unknown` type returned for JSON.parse. I'm almost surprised there isn't a standard stricter lib.d.ts, though I suppose that linters fill enough of those needs with their rulesets.
Looking at GitHub issues surrounding it, it doesn't appear that there has been an explicit ask to switch the type signature to use `unknown`, probably the next closest was this one, where `unknown` is mentioned in a comment as a possibility but the comment is not addressed in follow up discussion: https://github.com/microsoft/TypeScript/issues/26993
It would be a big breaking change though at this point, so they would probably be hesitant to do so. I thought I saw somewhere there was a proposal to add it as a compiler flag to treat all uses of `any` in lib.d.ts as `unknown`, but I have no idea what happened to that proposal and quick searches don't turn up anything.
The point of type-safety is to be able to describe more succinctly what the _programmer_ expects out of the code. As you said, when you get to production, "all bets are off". This still applies with any type-safe language. Types don't exist when you're compiling to machine code, either.
I'm not sure it is; the most dangerous things in programming are assumptions that are almost correct, so you don't notice the problem until it's too late. An error whose symptom is an Int actually being a String several files away from where the mistake actually happened can be harder to diagnose than the same error in a untyped language, just because it's so contrary to your expectation.
- Catch more errors at compile time
- Better tooling (better intellisense, “go to definition”, “find implementations”, etc.)
TypeScript provides both of these benefits just fine. Runtime types give you better runtime errors, and less undefined behaviour, but plain JS (or TS compiled to JS) does that bit just fine. TS types persisting at runtime could maybe be nice for certain use cases, like more powerful reflection, but that’s not type safety.
What’s a concrete example where your program would be more safe if TypeScript types were represented at runtime?
It also encourages better architecture in my opinion, as it forces you to reduce the importance of something being an exact type... As it's not strictly enforced at runtime.
If my parameter only accept "foo" or "bar" and that at runtime I send an invalid input, will I get a type runtime exception? Or will it silently allow the invalid input? If it's the former, that would be great news but then I wouldn't see the point in Io-ts, and where is the limit? Could they stop type erasure and allow generics reification?
> If my parameter only accept "foo" or "bar" and that at runtime I send an invalid input
calling a function with a general string argument when the parameter expects a string literal will have TS refuse to compile.
Most of the time you want to validate user strings/runtime generated strings. On most applications compile time strings are very few and not need validation. When you need validation of strings you generally need only runtime or both and then why duplicate the runtime validation with another compile time validation that take an alternative syntax? This seems 1) a niche feature And 2) a feature that users will misunderstand, the only dev I know which read about the feature think it works at runtime. Also I believe that existing template litteral do accept general strings I did a POC a few months ago so that's weird but maybe that TS can determine that a string is from runtime and then refuse it at compile time.
Some domains that will benefit: ORM mappers, CSS style generation, configuration strings, path munging, localization.
> Also I believe that existing template litteral do accept general strings I did a POC a few months ago
... yes, template literals accept general strings. Like I said,
> Template literals that contain non-string-literal parameters will still generalize to string.
the new feature kicks in when all the parameters supplied to the template literal are value types themselves, or an OR of value types (I don't know if something even more expressive is supported).
I hope you try to understand better what TS is offering before you make uneducated criticisms of it.
If you are strict, don’t intentionally try to outsmart the type system, and use manual runtime type checking when necessary [0], TypeScript does a good practical job of ensuring that you won’t run into runtime type errors.
The reason TypeScript doesn’t automatically include runtime type information is (probably) because the overhead of runtime type checking would outweigh the benefit when static type checking does a good enough job.
Also, JavaScript being a dynamic language has allowed some pretty complex patterns of types to emerge from real-world use cases that would be exhausting to enforce all the time at runtime. Typically in TypeScript, runtime type checking is (manually) done when the type system cannot otherwise confidently guarantee the type of a value. For example, it’s common to have a runtime check for the shape of a JSON api response because you’re not necessarily in control of what that will actually return. There are community supported packages to help make this easier [1].
0: https://www.typescriptlang.org/docs/handbook/advanced-types....
No, Typescript is very much just a static code analysis tool for JavaScript, there is no runtime at all, and there should not be one. It confuses a lot of people because they expect type guards at runtime, but it was never in the scope of the language to provide something like that.