Hacker News new | past | comments | ask | show | jobs | submit login

>You’re right on the first

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;
    }
  }
is identical to:

  class Foo {
    bar;

    constructor(bar) {
      this.bar = bar;
    }
  }
And this type error:

  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) => {};
is identical to this:

  const foo = (bar: Bar | undefined = someBarSatisfyingValue) => {};
is identical to this:

  const foo = (bar?: Bar) => {
    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.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: