Typescript was my stepping stone into the world of Rust.
Even before Deno made it easy, it was straightforward enough to configure a simple tsconfig and just run tsc. Much like cargo, there is a lot to be said for "it just works" tooling - especially for beginners or even new programmers.
It is probably fair to say it is one of the most influential and impactful languages of all time. There's even the future possibility of much of its type syntax being absorbed back into JavaScript: https://github.com/tc39/proposal-type-annotations
Opposite experience. Rust was my stepping stone into "hey, maybe JS will be better with some degree of static typing?" Yes it would. But not the way TypeScript does it though, and there aren't any other viable options, are there?
TypeScript brands itself a "superset" of JavaScript. In practice, it arbitrarily invalidates completely sensible JavaScript idioms.
There (kind of) is another option. It is called Rescript. But to be honest will probably never take off... :( For me it has by far the best type system for a script language. It is based on Ocaml. Its syntax is easier to read than Ocaml and as powerful as Ocaml if not more by aiming at JS world. One great feature you can take a look at to see how simple and powerful the type system is is Pattern Matching / Destructuring [1] and its "switch".
Strictly speaking, it takes exactly one such behavior (that you cannot even disable) for TS to stop being a superset of JS.
>although classes are out of fashion, so this is low impact
Looking at the TSC codebase, so are keyword arguments. The "in" thing is just to write very very long lines of multiple verbosely named positional arguments instead.
That said, "clases are out of fashion" is a complete non-argument. I'm of the functional persuasion, yet I've found that classes are the ony way to write TypeScript that fits on your screen at all.
Especially now that classes are being introduced in JavaScript proper, and of course TypeScript does them only slightly differently (handling of default property values and "definedness" differs).
>On the second, doesn’t this work?: ‘function foo({ bar = 3 }: { bar?: number })’
You also need a `= {}` there, otherwise you'll need to call `foo({})` - it won't let you call `foo()`. This is also in JS though, so a bad example of TS breaking things (`function foo ({ bar })` still won't work though). There are probably better ones that people encounter, work around, and forget about, because nobody's listening anyway. "The code making sense is not important, what's important is helping the user" lol.
Now imagine how the above looks with 5-10 kwargs (because keeping context in the class instance is "out of fashion", so it's either a ton of args per function or a "context" record which is effectively reimplementing classes but with a worse experience), and an aggressive formatter insisting every individual thing has to be in its own line.
Here's another: failing to infer the type of `this.constructor`.
Sure, the constructor signature may change in a subclass (why not disallow incompatible constructor overrides, given incompatible property/method signatures are already disallowed?); then what about static methods accessed via `this.constructor`?
So you end up defining an interface type for the constructor and using `(this.constructor as MyConstructorType).staticMethod` or whatever. Which is just visual noise where the fucking intent of the code was previously clear as day, so clear that TSC should've been able to infer it (yeah the type inference also sucks).
Also, ever seen TS2322? It's my pet now. What it do
All in all, TS really puts the "Java" back in JavaScript, and then some.
EDIT: Also crap like not being able to have a question mark and a default value in positional arguments so you gotta add `|undefined` there. Even the stuff it adds on top of JS is poorly thought out.
> I'm of the functional persuasion, yet I've found that classes are the ony way to write TypeScript that fits on your screen at all.
Huh? I’m of the functional persuasion too, and I use classes in TS too, but for strategic reasons (well defined value objects are easier to reason about than duck typed POJOs, and they perform better too). But I’ve never found them more space-dense than the equivalent function-only code. Often quite the opposite, as so many functions’ return types can be fully inferred. Which of course, this is how you get ~~ants~~ duck typed POJOs, but you can’t have explicit type defs without explicit defining them somewhere, and of course the syntax that collocates the field type and its value is more dense than the syntax which is wholly incompatible with that concept.
> handling of default property values and "definedness" differs
The only difference is that TS provides a fully optional shorthand for assigning both the type and value in constructor arguments. The actual behavior isn’t any different. This:
class Foo {
constructor(readonly bar: number) {}
}
is identical to:
class Foo {
readonly bar: number;
constructor(bar: number) {
this.bar = bar;
}
}
class Foo {
readonly bar: number;
constructor(bar: number) {}
}
is identical to this type error, just caught sooner:
class Foo {
bar;
constructor(bar: number) {}
}
const foo = new Foo();
foo.bar.toFixed(2);
> Sure, the constructor signature may change in a subclass (why not disallow incompatible constructor overrides, given incompatible property/method signatures are already disallowed?)
Because the compatibility is checked on the `super` call which is required both at compile time and runtime, and because many use cases for subclasses are impossible or even invalid without different construction contracts.
I know there are many strong feelings about examples like this being “wrong”, but it’s a common enough inheritance example to illustrate the point:
class Square extends Rectangle {
constructor(/* ? */) {}
}
You cannot satisfy both Square and Rectangle with the same constructor arguments. This of course bolsters the point that this inheritance model is “wrong”, but it’s exactly right according to the domain, and the equivalent functional code to calculate eg area would similarly have to be either polymorphic over different shapes or expect a single shape constructed with different parameters.
> this.constructor
You got this one right, and it’s worse than you describe because of the weird rules for where you can’t have type parameters or explicit `this` types. The workaround is to use a static factory method, but it’s a shitty workaround with a lot of ceremony to do something that TS generally does well: model the types of real world JS code.
> Also crap like not being able to have a question mark and a default value in positional arguments so you gotta add `|undefined` there. Even the stuff it adds on top of JS is poorly thought out.
Ima help you out! Assuming your default satisfies the non-undefined type, you can just skip the union, it’s implied. This:
const foo = (bar: Bar = someBarSatisfyingValue) => {};
>you can’t have explicit type defs without explicit defining them somewhere
Sure, as long as I have to define them once and exactly once. Not always possible, as in the case of simple, garden-variety keyword arguments.
// foo is required, bar has default, baz is optional
type POJO = { foo: number, bar: number, baz?: number }
function myFn ({ foo, bar = 3, baz }: POJO = {}) {
// oh wait...
function myFn ({ foo, bar, baz }: POJO = { bar: 3 }) {
// oh wait...
function myFn ({ foo, bar, baz }: Partial<POJO> = {}) {
bar ??= 3
if (foo === undefined) throw new Error("type safety")
// ...oh.
// maybe?:
function myFn ( foo, bar, baz }: POJO = { foo: undefined as never, bar: 3 }) {
// try :D
Technically the destructuring and the type declaration are completely separate things ofc (that just happen to look about the same because they're isomorphic but that's a watchlist word).
But... it doesn't even try to infer the type of an untyped destructuring - even if it's a local function used only once!
>well defined value objects are easier to reason about than duck typed POJOs, and they perform better too
Long live those!
class BaseValueObject {
constructor (values: Partial<this> // oh wait...
> Assuming your default satisfies the non-undefined type, you can just skip the union, it’s implied
type Foo = { defaultBar?: Bar }
function main (foo: Foo, bar?: Bar = foo.defaultBar) {
// oh wait... parameter can't have question mark an initializer
function main (foo: Foo, bar?: Bar|undefined = foo.defaultBar) {
// this works but is silly and scaryish
>The only difference is that TS provides a fully optional shorthand for assigning both the type and value in constructor arguments. The actual behavior isn’t any different
That's what a sane person would assume, no? Well, allow me to disappoint you (like that ever needs permission):
$ node
> Object.getOwnPropertyNames(new class Foo { a })
[ 'a' ]
$ npx ts-node
> Object.getOwnPropertyNames(new class Foo { a: any })
[]
Probably because it compiles them to a pre-standard, ES5-compatible class implementation based on good ol' `Foo.prototype`. And since they've already handled them one way, they can't become spec-compliant without breaking backwards compatibility.
The other place where this shines through particularly egregiously is the support of ESM static import/export. Everybody's build tools been compiling that back down to CJS so hard that Node.js 16+ introduced intentional incompatibilities between CJS and ESM modes just to get people to finally switch to the standards-compliant module system. So you end up in a situation where the library is written in TypeScript with ESM syntax but the only available browser build is a CJS blob which completely defeats the main touted benefit of static imports/exports, namely dead code elimination...
So you decide what the hell, let's switch TSC to ESM and moduleResolution node16, and end up having to use something like https://github.com/antongolub/tsc-esm-fix because the only allowed fix for TSC doing the wrong thing is at the completely wrong level - https://www.typescriptlang.org/docs/handbook/esm-node.html - if you don't see what's wrong with that, you're one of today's lucky 10000...
> Not always possible, as in the case of simple, garden-variety keyword arguments.
// foo is required, bar has default, baz is optional
All of your examples are correctly identified by TS as type errors, because they all have a default argument which will never bind `foo`. True in JS as well as TS. Consider the untyped code, with some access to a required `foo`:
function myFn ({ foo, bar = 3, baz } = {}) {
return bar + (baz ?? foo);
}
myFn(); // NaN
Your function shouldn’t supply a default argument, because if the foo property is required the object containing it has to be too. It should instead supply defaults to the properties in it. This is closer to what you seem to want:
function myFn (pojo: POJO) {
const { foo, bar = 3, baz } = pojo;
// foo is required, bar has default, baz is optional
return bar + (baz ?? foo);
}
myFn(); // Compile error
myFn({}); // Compile error
myFn({ foo: 2 }); // 5
myFn({ foo: 2, bar: 4 }); // 6
myFn({ foo: 2, bar: 4, baz: 6 }); // 10
myFn({ foo: 2, baz: 6 }); // 9
> But... it doesn't even try to infer the type of an untyped destructuring - even if it's a local function used only once!
Yes, it does, if the thing you’re destructuring is typed. It doesn’t infer from usage, and while that sounds nice and some languages have it, it's fairly uncommon.
> oh wait... parameter can't have question mark an initializer
Yeah. It can’t. But if you applied what I suggested, it compiles and has the type you expect without adding undefined:
type Foo = { defaultBar?: Bar }
function main (foo: Foo, bar: Bar = foo.defaultBar) {}
type Main = typeof Main; // function main (foo: Foo, bar?: Bar | undefined) {}
> Probably because it compiles them to a pre-standard, ES5-compatible class implementation based on good ol' `Foo.prototype`. And since they've already handled them one way, they can't become spec-compliant without breaking backwards compatibility.
You’re partly right. I’m on mobile so I can’t dig into the failure but type checker seems to be crashing or getting stuck due to the confusing syntax. If you make it more clear by putting parentheses around the class expression, you still won’t get any compiler errors because it’s constructed with no arguments, and a is implicitly assigned undefined which satisfies any. If you then give the a property a non-any/unknown type you’ll get a compile error because a wasn’t assigned.
It’s weird that even newer compile targets don’t get an assigned a: undefined at runtime, and definitely qualifies as a compiler bug (you should file it! I’ll add what I’ve learned!). It certainly does if you actually assign anything to a during construction.
> Everybody's build tools been compiling that back down to CJS so hard that Node.js 16+ introduced intentional incompatibilities between CJS and ESM modes just to get people to finally switch to the standards-compliant module system.
This is factually false. CJS is fundamentally incompatible with ESM, and has been since day 1. They shipped it incompatible from at least Node 12 because there’s no way to make it compatible. ESM is fundamentally async, and CJS is fundamentally blocking. ESM imports are “live”, CJS are static values at the time you call require. They have fundamentally different module resolution algorithms. All of this has been documented in Node also since at least v12, and has been spec compliant (notwithstanding since fixed bugs) the whole time.
There are definitely valid gripes about how TS has supported ESM though, particularly in terms of file extensions. Thankfully they’re actively working to address that now.
>Thankfully they’re actively working to address that now.
Where? The response I saw on GitHub issues (to the few people who considered it worthwhile to be vocal about the issue) was literally (well, paraphrased): "yeah we did this wrong but we're sticking to it anyway" (because of MS-internal org inertia I assume, something that the TS devs surely have to account for but it's hidden from us as an "open source" downstream)
I’m going to bed but don’t want to leave this unanswered before I lose track of it. They’re working on it as part of the next major release and you can see that in the roadmap they’ve posted for it.
... as comments. Which superficially resemble the syntax of type hints but do nothing. Which has to be one of the worst language design decisions of all time.
I'd disagree that its a poor decision, some help is better than no help when reading code - and if it's inline then it's intrinsically meaningful.
By no means is it perfect, the language doesn't enforce the type rules, but that the developer has the option is far better than not. As far as I can tell the proposal would pretty similar to Python; one can misleadingly or accidentally misuse type hints like the following:
But when done right, it gives you a leg up when you come back to read your own code or scan over someone else's. When you're reading over other's code in group programming assignments, comments make a world of difference (a niche example).
It is a choice, but its use in code might just inspire someone to investigate further - my own introduction to types in programming came when I wondered what these oddly placed colons and arrows were in some Python I came across. If they do add these annotations to JavaScript, some future programmer browsing the source of a web page may well just stumble upon types and have a whole new world of theory opened up to them - I'm all for it.
That's fine for atomic types like string or int, but typescript goes a lot deeper than that with powerful generics. For example, I'm working on a library of functional data wrappers. This would be useless without Typescript and impossible to type without generics.
There's also no way to share and reuse types in comments. Do you copy paste comments to "type" things? What about complex objects? What happens if the comments get out of sync? How do you test them?
I used to be a typescript skeptic too, but much of that skepticism tends to come from drastically underestimating the power of the TS type system.
I just want to note that it seems incorrect to call this a superficial resemblance in terms of syntax. I went and read through the whole thing, and the syntax for the type hints seems to be as close to 100% TypeScript as possible, which is to say, it is very close. There may be some features missing that TS supports, but the bulk of it is there, and what's there is basically Typescript, which is the best thing that could possibly be accomplished here.
Typescript has been doing a fantastic job, and this proposal is continuing in that same vein, truly absorbing as much of that as possible back into JS!
Kudos to everyone involved, great effort!
And re "[the type hints] do nothing":
The section at the end of the proposal clearly explains why that must necessarily be the case: Evolutions of JS must not break the web (especially) for the users.
Quoting: "TypeScript's model -- which has been highly successful for JS developers -- is around non-local, best-effort checks. (...) Additionally, defining a type system to run directly in the browser means that improved type analyses would become breaking changes for the users of JavaScript applications, rather than for developers. This would violate goals around web compatibility (i.e. "don't break the web"), so type system innovation would become near-impossible. Allowing other type systems to analyze code separately provides developers with choice, innovation, and freedom for developers to opt-out of checking at any time."
So this proposal provides the best possible path forward for JS, based on what folks are voting for with their feet by using TS: Making JS compatible with TS-style type hints that can be used by external tools (i.e. not the JS engines executing the code at runtime) to validate the code in a best-effort manner while the developer is looking at it, while not changing JS runtime semantics and thus never breaking the code while the user is running it.
I don't see the problem with this design decision.
I'm not the biggest fan of static type checking (especially in a language like Javascript, where it isn't used for safety guarantees), but it has its uses.
The "as comments" part of it isn't about syntax. They really mean no runtime overhead/behavior. (I don't think they should have put it that way -- it's just going to cause confusion.)
Is there really any question that "no runtime overhead" needs to be an option? (I think any proposal that didn't include this would be DOA.)
It also seems clear to me it needs to be the default option, for multiple reasons (language changes should be backwards compatible as much as possible, it should be easy to adopt new language features incrementally, type usage is an application-level concern.
Note: there's nothing in this proposal that prevents run-time enforcement. Good design limits scope but keeps your options open.
I would prefer if they actually had a purpose in the language in which they were used (JS.) Add optional type hinting to JS itself, if it's that important, don't add pretend type hinting to it because typescript users don't want to have to use their compilers anymore.
I know a lot of people like it but it just seems like an ugly hack to me, extra syntax for the sake of an abritrary third party tool. I know I'm a dinosaur. I want to go back to the days of JQuery modules and FTP. Feh.
Is there any news about the status of the type-annotations-as-comments proposal?
The README has said "Details will change in the coming days" since March 31. I know TCs are supposed to move deliberately, but this is such a neat idea I'm impatient.
Even before Deno made it easy, it was straightforward enough to configure a simple tsconfig and just run tsc. Much like cargo, there is a lot to be said for "it just works" tooling - especially for beginners or even new programmers.
It is probably fair to say it is one of the most influential and impactful languages of all time. There's even the future possibility of much of its type syntax being absorbed back into JavaScript: https://github.com/tc39/proposal-type-annotations