This is a very outstandingly interesting line out the whole writeup.
I like the writeup in its entirety for being very balanced and thoughtful, but this line in particular really stands out to me as worth more thought for anyone interested in language and type system design.
Inference is great.
Except... when it's not. When it's "too much". When it starts making breaking changes appear "too distantly".
It's an interesting topic to reflect upon, because the inference isn't making a breakage; just shifting around where the breakage appears. (And this makes it hard to ever direct criticism at an inference mechanism!) But this kind of shifting-around of the breakage appearance can be a drastic impact on the ergonomics of handling it: how early it's detected, how close to the important change site tooling will be able to point the coder, etc. That's important. And almost everything about it involves the word "too" -- which means the area is incredibly subjective, and requires some kind of norm-building... while not necessarily providing any clear place for that norm-building to center around.
I don't have a point here other than to say this is interesting to reflect on. I suspect the last chapter on type inference systems has not yet been written. Can an inference system be designed such that it naturally restrains "too" much use of it?
Here's a related property I've also discovered of 'advanced' type systems.
In my understanding of how unification happens in systems like H-M, you first give every expression its own type variable, then perform a search to produce assignments to those that remain valid and leave the remainders generic.
But as your type system gets fancier, and particularly in the presence of union types and subtyping, this search process can feel like "make whatever type judgements are necessary to make this program still compile". E.g. in code like
let x = new Set();
x.add('hello');
x.add(3); // oops, meant to add the string '3'
f(x);
The inferencer can just reason "oh apparently x is Set<number|string>", and "oh apparently f() operates over all kinds of sets, Set<{}>". And especially when there's never a more specific type for things to "bottom out" on (like say f just forwards the set elements to JSON.stringify(), which accepts both string and number already), nothing will ever reveal to you that you actually wrote a bug.But then meanwhile even in Haskell you run into cases where the inferencer wasn't generic enough, like https://wiki.haskell.org/Monomorphism_restriction , so it's not even clear cut that you want the inferencer to be smarter or dumber. As my coworker says: as a programmer you have to be able to basically run the inferencer in your head, and that becomes very hard when the inferencer gets very smart. (See the RxJS bug in the above blog post.)
During development, I like to keep my types unspecified until I'm confident that things have congealed properly. But before merging, I like to make sure and add type annotations to anything that's meant for public consumption.
Partially for readability and documentation. Partially because adding those annotations ensures that the type checker and I are on the same page. But mostly because those manual type annotations represent a promise that I, the programmer, have made about the current and future behavior of a certain chunk of code. Once those promises have been codified, then the type checker has my back and can help warn me if I'm about to break one of them with a careless change at some point in the future.
In Rust, inference is limited within functions. The language doesn't allow inferring the argument or return types of functions to avoid this kind of action at a distance.
The function API is just one of the many arbitrary places where one can limit inference and require users to provide types. In other languages the module boundary might also be an appropriate place to do that.
This way, when you write
const f = (a, b) => a + b
the compiler can tell you to write: const f => <T extends number|string>(a: T, b: T): T => a + b
or const f : <T extends number|string>(a: T, b: T) => T
= (a, b) => a + b
This would be effectively like GHC's typeholes, where the compiler will tell you what to write when you say f :: _
f a b = a <> b
except that rather than being an optional step by a developer, it's a mandatory standard.(Well, I don't think typescript infers that type for that function, but whatever it actually infers, that's what should be there in the error message.)
Personally, I experience this quite often, e.g. in Java whenever arrow functions are heavily used, such as with streams.
It would be nice to have compilers report the whole inference chain somehow, just to be clear about all possible spots where things might be wrong. But I suppose that's difficult to visualize in a well human-readable way.
It's not. The type-checker must have constructed a call stack of inferred types in order to find the violation, so it simply has to print all the calls (source line and line number) and the types it deduced, and let the programmer (or IDE) compare that to the code in context and look for surprises.
1) Make types explicit (or generic) at any interface boundary
2) Periodically query the inferred types of things from my editor just to make sure they come out to what I expect
3) If an error message feels like it's in the wrong place or an inferred type isn't what it should be, progressively make things more explicit until the problem is resolved
But you're right, that's all very subjective and dependent on learned norms.
That's a very valuable insight, and a concern in a lot of languages with advanced inference!
The problem is exacerbated in Typescript though because it is fundamentally a practical, evolved layer over Javascript, with an unsound type system that likes to bail out to any.
Is this some TypeScript joke I'm too Hindley-Milner to understand?
It's an interesting goldilocks paradox likely only exacerbated by the size of Google's codebase and variety of developer code styles/inferencing preferences.
and the source is here:
https://github.com/burtonator/polar-bookshelf
I could have NOT made as much progress just by using JS directly. When you have a large code-base and you're trying to make progress as fast as possible refactoring is needed and the strict typing is invaluable.
Honestly, the MAIN issue with TS are issues around webpack + typings for 3rd party and older modules.
I'd say 85% of the code I want to use already has types but when they don't it's frustrating to have to pause and write my own types.
I have 20 years of Java experience. Used it since 1.0 and for the most part have been unhappy with everything else.
I've decided that Node + Typescript is by far the most productive environment for me to switch to. I can code elegant apps with both front and backends and I get strict typing.
Could NOT have made so much progress without TS.
Are you using Angular as your frontend?
I feel a lot less productive without it and the interaction with React has been perfect in my view for the last couple of months.
With hooks allowing a lot more inference in typing and removing most need for Higher Order Components, there aren’t any places where the typing feels like a big impediment. It’s definitely a lot better than it was, say a year ago.
When I first started using it I had lots of `any` in my code (like the Google employee is describing here). But over time it really starts being extremely clean.
It may be an overly broad request, but I'd be very interested if anyone had any suggestions for how we might go about performing such an upgrade in an iterative manner. My understanding is that pre-1.0, TypeScript went through a number of breaking changes (as would be expected of any pre-release software), but I've never found a complete list of what those breaking changes were.
I'm currently trying to find a way to write (type safe) business logic once, then reuse it pretty much everywhere (mobile / web / desktop),and it seems to me that typescript has become the only option. Javascript runtime is present everywhere, and can interface with anything.
Does someone knows of another alternative (viable right now, or in the coming months) ? I know llvm can theoretically target any platform, including wasm, but how painfull is it in practice ? Can you write a line of code that does a network request then expect it to run as it is on the browser and on mobile platforms ?
Also, as i mentioned in another answer, i'm not only concerned about being able to run the code on another platform, but also having it run on an "friendly" environment (with some kind of cross-platform I/O api).
If you want to make it work with Ionic/NativeScript/Electron as well use xplat. https://github.com/nstudio/xplat
The ReasonML native tooling (i.e. non-web) is evolving, but it's fundamentally sugar on top of a long solid history of OCaml.
I'm not really looking for a GUI abstraction layer, but i'd like to be able to write to a file, or perform a network request, in a platform-independant way. I'm afraid this requires a little bit more than just a javascript transpiler target.
With the experience I've found that most of the type errors are actually between the backend and the frontend in web applications. It's still hard to fully type the entire flow from the database calls with the ORM to the objects manipulation in the frontend.
How are you dealing with that? We used Nexus with GraphQL but it was still a bit cumbersome.
Having types straddle the client/server divide is a huge win
This very well could be the reason Flow has been steadily losing mindshare in favor of a total TypeScript monopoly. TypeScript is great, but lack of diversity is a shame regardless.
Flow has built in support for the Language Server Protocol. tsserver doesn't even support LSP. I agree ts has better IDE support but that's in part because they don't use LSP. odd they expect others to follow it when they don't.
That said, we do rely on automated error excludes (similar to eslint-ignore-next-line) for things that cannot be fixed with codemods. Those errors were always there it’s just now you know about them. Better to stem the bleeding by updating the type checker to the latest version.
To be fair, many Flow upgrades are easy, yes. But some are absolutely nightmarish. The 0.85 upgrade was especially painful - it involved some very non-trivial codemods, we couldn't get it right in all cases, and it involved some loss of type safety as well :(
I've also found Flow to be painful to work with when dealing with a multi-repo setup which exports types from source code. It becomes unnecessarily hard to make libraries interoperable because a Flow upgrade in one project can cause a cascade of errors deep in another project's transitive dep. One basically has no actionable recourse other than waiting until all her deps are version-aligned on Flow (or never upgrading).
In that regard, the Typescript pattern of using .d.ts files as a boundary between a library's tsc version and the consumers's tsc version is quite nice and something I'd like to explore more w/ Flow.
I'm not sure you're being entirely honest here, as we both know through feedback that flow updates in the said monorepo are one of the most burdensome processes for its contributors.
As a maintainer/owner of a monorepo experience, it's crucial to maintain empathy and honesty of how processes such as dependency and tooling updates affect (and are perceived by) its users.
Typescript does not have soundness as a primary goal for a very good reason: there's a trade-off between soundness and productivity, especially when threading the large gray area between theoretical soundness and the realities of what constructs the compiler implementation is actually able to handle and what the existing ecosystem throws at it.
What the GP is lamenting is that a lot of changes in Flow create more work for developers, but they don't improve the rate at which it catches bugs in practice.
> mySel: d3.Selection<HTMLElement, {}, null, undefined>;
I'm curious how Google and others approach adopting Typescript gradually, as I'm pretty new to it, I'm assuming it goes like: The programmer converts code to Typescript and when they come across return types they copy the inferred type and add it to the codebase directly wherever possible. I'm assuming just as a matter of using (untyped) libraries you need to rely on the output of Typescript in order to try have every return typed.
So the biggest problem seems to be how TS infers things changed meaning you can't always trust what you copied as staying consistent, even if the source library doesn't change itself. That's always something to keep in mind for overhead.
<button (click)="login(email, pass)" />
And then a TypeScript function like: login(email: string, pass: string) {
}
TypeScript can't help you at all here because all the typing is determined at runtime by Angular. Even if `email` is a number or a boolean, no problem, it will just happily pass it in.What benefit, then, does TypeScript provide? I understand it's compile-time guarantees, but how does that help if the types are coming in from HTML land which the TypeScript compiler doesn't examine at all?
That's not really a typescript issue though, and it works great with libraries that don't use string templates. From what I've seen the angular community hasn't really prioritized a typecheck-able templating language.
Disclaimer: I have heard about the above enough to dig up a random blog post about it but I don't know a lot about how it works.
I think in JSX (and I've seen experiments with lit too) there is also integration between the template and type checker.
TypeScript does not come with a minifier. The code it produces is compatible with the target version of JavaScript (e.g. compile ES6 modules into CommonJS) but unminified.
TS pointedly does not minify output. It does the opposite: try to generate code that a human might have written.
It's very easy to incorporate TS into a Babel or Webpack build pipeline though, so you can use a purpose-built minifier of your choice.
They work with Closure-annotated JS (with JSDoc) though; not sure if it fits your use case.
It's a bit difficult to hook up though, and we haven't had the cycles to make the open source version user friendly, I'm afraid :-(
This isn't TS-specific; it's common in Python too: coercing to Bool has language semantics (for whatever language you are in) which often don't match the application semantics of your program. Application programmers don't (and shouldn't have to, but for the language's over-eager coercions) always think about the boolean semantics of all their objects. In particular, None and empty/zero object are both False in Python, and Python style/linters push you to avoid explicit comparison to None, which gets weird when your application wants to treat empty objects as True because they have differen semantics from None. (For example, in a security function, None may mean no-op / fallback to default, but Empty might mean "Reject all".
From my point of view Core members of any super large project (like React, Angular, TypeScript) are limited by design in what they perceive as their target audience and their use cases. This is simply a matter of fact: even as a core dev you cannot know how every dev uses your product.
So this is some sort of left-pad moment for TypeScript.
Google has a lot of tooling and some very thoroughly-considered and reinforced policies and cultures around their use of a monorepo. Trying to use a monorepo without those tools and ingrained policies may not be very likely to lead to similar results.
While what you said is true, the codebase of most organisations is not large enough that they run into those scale constraints for a long time. Instead, if you split up your code you immediately get organisational headaches of managing changes across multiple codebases. If you keep it together, the scale challenges can be managed later, if they occur (c.f. YAGNI).
If you are splitting, codebases should be split across organisational boundaries, not technical ones. If you look at the FOSS world, the obvious conclusion is that each library is in its own repo, and that is partly true. In practice what is happening is that each OSS project is its own organisational team, and so it lives together. If you have parts of your company on different continents working on different things (and maybe you need different access) then splitting up may make sense. Otherwise, you're probably just making your life harder for little to no gain.
https://opensource.google.com/docs/thirdparty/oneversion/
That's why the typescript upgrade was so hard for them. We (attempt) to enforce a single version of a library/toolchain to be checked into the codebase at any given time. You can have multiple in during an upgrade, but it's highly discouraged.
Bazel is extensible via rules [1], so if you really wanted to use NodeJS on your team, you might create a `nodejs_binary` rule that put everything in the right directory and ran some NodeJS packager on it. You'd probably not put it into production.
Also, third-party code lives in a single third-party directory, so yes, internal users could pull down code they wanted (and for which there wasn't a satisfactory internal version already) into that directory: https://opensource.google.com/docs/thirdparty/
[1]: https://docs.bazel.build/versions/0.29.0/skylark/rules.html
Something like this might only be possible due to their tooling and test coverage. So when you change something, you immediately get alerted of broken tests.