55 comments

[ 3.2 ms ] story [ 117 ms ] thread
Looks nice. I love how dedicated typescript is to sticking to standard javascript, even by championing features through the ecmascript standards track.

They understand very well how bad it would be if the two languages were to diverge.

Yes, I agree. I also think that if they were starting out today, they wouldn't have either Enums or Namespaces since those features require adding runtime code (as opposed to every other TS feature which is erasable). However, when they started out, JavaScript wasn't as evolved (and it didn't look like it would evolve this much) and so they had to add a few things "on their own".
I started writing this comment thinking "no way, enums make so much sense", but then remembered Flow, where an enum is modelled simply as a union type, and agree that that approach would have probably been better.
Yep, and nowadays TS has string literal types and unions so enums are pretty much obsolete.

    type Direction = "up" | "down";
Enums and string literals aren't completely interchangable.

Enums are transpired to objects so you can iterate the keys/values. You can't do that with SL which are a transpiler feature only.

Also you can't codedoc string literal values like you can with enums where it shows up with intellisense.

I tend to prefer enums as a result

(comment deleted)
Since TS 3.4 you can have enum-like behaviour by using the `as const` annotation on object and array literals.

    const types = ['x', 'y', 'z'] as const;
    const a: typeof types[number] = 'a'; // not ok
    const b: typeof types[number] = 'z'; // ok
That is correct. However, I still prefer using union types so that I'm not locked in to TypeScript.

By using only union types as "enums" and never using enums or namespaces, I guarantee that all the TS-specific code I write can be erased and if I ever were to use just JavaScript in the future, I could.

Also, I use Babel to transpile TS to ES3.

Interesting. I swear that when I previously tried this approach I had to explicitly tell typescript that a string is indeed of a particular type:

  const test(direction: Direction) => direction;
  test('up' as 'up');
But now that I'm testing in typescript playground, I no longer need to write the "as" part — simply `test('up');` works fine as well.

I wonder if you've seen this quirk of typescript before.

I have.

It's when you do

    const dir = "up"; // TS infers type "string"
    test(dir); // error
For mutable variables (let/var), you usually want TS to infer `string` in the first line. IMO the fact that they also infer `string` for constants is a design flaw, that I hope will one day be fixed.

They recently added a feature to make this less painful:

    const dir = "up" as const;
    test(dir); // works

That's the same as `as "up"` bit without having to duplicate the string.
Note that TypeScript also has union types - I never use enums.
It just means we are at embrace & extend. And personally I can't wait to hit extinguish. Good riddance "ECMAScript".
Really nice, I work in a project with a lot of nested optional values, and all the `a && a.b && a.b.c && a.b.c.d` checks really introduce a lot of noise.
Mostly it's a code smell though

https://en.m.wikipedia.org/wiki/Law_of_Demeter

4 chains is like reaching into an XHR response to pull out the success result, so I wouldn't say that's necessarily a smell.
Not really, especially when you're working with external APIs. That's when I find myself using a lot of this type of code.
I'd say it is a design smell and not necessarily code smell. Any API or data interface should try to keep "optional keys" at a minimum.

Depending on the language, `a.b && ...` could be checking either or both of the following:

- does `a` have key 'b'? - is `a.b` truthy?

Tell that to the team that created protocol buffers.
Programming is fundamentally about the transformation of data and sometimes data is in such a shape that inevitably requires such code. Most of the time, you don't get to decide what shape the data is in.

OOP just obfuscates this basic fact of programming life to the point where it's not obvious anymore. That is not a good thing, it's a huge waste of time. Favor plain data over "objects" and functions over methods. Favor plain SQL queries over ORM.

I don't think it's bad enough to warrant introduction of new syntax sugar to make it shorter.

The problem might be that we have most likely not yet handled the edge case situation where a.b and a.b.c doesn't exist. And just want to log out the value, and print undefined if it doesn't exist.

I disagree. This is just an extreme example, but take it down one or two levels and it's extremely common.

Ruby has had this kind of syntax since forever and it's one of the things that people love about it.

> The problem might be that we have most likely not yet handled the edge case situation where a.b and a.b.c doesn't exist

That's not an edge case, that's an expected case. If it doesn't exist, nothing is supposed to happen.

optional chaining !!! FTW
"Assertion Functions" are quite interesting. When we moved from Flow to TypeScript, this was the number 1 thing that frustrated us about TypeScript. The issue is that we had a lot of calls like this:

``` function assert(condition: boolean, message: string) { if (!condition) { throw new Error(message); } }

assert(x !== null, "x can't be null");

// Flow understands that x can't be null here because `invariant` throws an error otherwise, but TS doesn't ```

However, TypeScript couldn't understand that `x` can't be null after that call since it doesn't understand what `assert` does. For that reason, we had to write a codemod that replaced all of our `assert` calls with an if condition that threw an error inline.

The new `asserts` syntax is nice (it would be even better if TS could do this automatically like Flow though), and will be a much welcome update for us.

I see you used flow, what do you think about nominally VS structurally typed classes?
I don't have much of an opinion here since we don't use classes that much (we use object types for everything). I think I prefer structurally typed classes but mostly just because that makes them consistent with how objects work (structurally typed).
I wish though that TS would verify that the assertion functions really check the things that they claim to check. At the moment you can write

    function assertIsString(x: number | string): asserts x is string {
    }
and TS will just ignore the fact that you didn’t check anything. One shouldn’t have to introduce a giant hole in the type system just to be able to encapsulate checks that TS should be capable of understanding.

I filed this as a bug to see if the developers agree: https://github.com/microsoft/TypeScript/issues/33743

if that were possible you wouldn't need the special annotation
What do you mean? Without any special annotations, TS already understands that

    function test(x: number | string): void {
        x.toUpperCase();
    }
is an error and

    function test(x: number | string): void {
        if (typeof x === "number")
            throw new Error();
        x.toUpperCase();
    }
is not. So it should just as easily be able to understand that

    function assertIsString(x: number | string): asserts x is string {
    }
is an error and

    function assertIsString(x: number | string): asserts x is string {
        if (typeof x === "number")
            throw new Error();
    }
is not.
if TS can recognize the difference between the last two, why have an `asserts` annotation at all?
So that one can extract out a repeated check such as

    if (typeof x === "number")
         throw new Error();
into a function.
I think the thrust of the question is: why can't TypeScript automatically infer the assertion, rather than needing to write out a special annotation on the function?
It could, but as a design decision, TS never infers function types based on function bodies, to keep inference more local and predictable. You're expected to specify argument and return types from outside (and TS will then check the body against them). So it's reasonable to expect you to specify assertion types from outside (and TS should then check the body against them).
Fair point. In that context your question makes sense.
This is incorrect: TypeScript doesn't require annotating functions with return types, and can infer return types from function bodies.

It doesn't infer argument types, but it does infer return types.

You’re right, I must have been misremembering from a bug where return type inference was failing.

So yeah, TypeScript doesn’t but perhaps should infer assertion signatures where a return type is not specified. However, it would be bad if people who do want to specify the return type were unable to do so, so an `asserts` annotation syntax would still need to be available.

> it would be even better if TS could do this automatically like Flow though

That would imply that the same function signature could produce different type checking results at the call site. That is something I find extremely frustrating as you can no longer reason about type checking locally.

I don't get Assert Functions.

What are they good for? Maybe there is a use case I am not getting, but this

  function yell(str) {
    assert(typeof str === "string");

    return str.toUppercase();
    //         ~~~~~~~~~~~
    // error: Property 'toUppercase' does not exist on type 'string'.
    //        Did you mean 'toUpperCase'?
  }

  function assert(condition: any, msg?: string): asserts condition {
    if (!condition) {
        throw new AssertionError(msg)
    }
  }
Seems like the equivalent to

  function yell(str : String) {
    return str.toUppercase();
    //         ~~~~~~~~~~~
    // error: Property 'toUppercase' does not exist on type 'string'.
    //        Did you mean 'toUpperCase'?
  }

But the latter has much less code and is clearer too.

What am I missing?

I’m not sure that’s what you’re asking, but assertion are often used to trigger error in debug mode on runtime properties not covered by the type system (array having a min / max number of elements, code being executed in a background thread, etc..).
I still don't get it. Please help me out.

Why should the type system have anything to do with something like this?

  function something(num : number) {
    assertIsInRange(num);
  
    return num + 1;
  }
  
  function assertIsInRange(num: number, msg?: string): asserts num {
    if (num < 50 || num > 100) {
      throw new Error('not in range');
    }
  }
  
  something(50);

How is `asserts num` helping here?
If your something function was `something (num: string|number)`, typescript can safely trust that`num` is a number in the block proceeded by your call to assert because it knows it must be a number, not a string.
This, and undefined values. Will be of huge value for dealing with protobuf data, where all keys are optional.
If the `something` function had `(num: string | number)` I would not be able to call `assertIsInRange(num: number)` because `Argument of type 'string | number' is not assignable to parameter of type 'number'.`

So If I wanted to do that I would have to

  if (typeof num === 'number') {
    assertIsInRange(num);
  }
anyway.

I think I still don't get it.

i’m not sure about typescript, but in many languages assertion have the natural behavior of being compiled out of release builds and kept only for debug mode.

The fact that it’s a built in feature of the language also can sometimes helps the compiler infer what’s the properties of code after it. But theoretically , yes you could very well code some equivalent testing function yourself, except for the compiler inference part it would probably be equivalent.

> i’m not sure about typescript, but in many languages assertion have the natural behavior of being compiled out of release builds and kept only for debug mode.

Not in TS. This code

  function something(num : number) {
    assertIsInRange(num);

    return num + 1;
  }

  function assertIsInRange(num: number, msg?: string): asserts num {
    if (num < 50 || num > 100) {
      throw new Error('not in range');
    }
  }

  something(50);
gets compiled to

  function something(num) {
    assertIsInRange(num);
    return num + 1;
  }
  function assertIsInRange(num, msg) {
    if (num < 50 || num > 100) {
      throw new Error('not in range');
    }
  }
  something(50);
> The star of the show in optional chaining is the new ?. operator for optional property accesses.

Wow, that took a while! For already about 9 years this feature is in Coffeescript. And indeed it must be the star of the show because once you're used to that you almost can't live without it. Why did JS/ESxx never copied that idea?

edit: just read it has just been added to JS some days ago as well..

As an aside... I am still a huge fan of Coffeescript as my main driver. I'm learning more about TypeScript and Rust, but Coffeescript remains my language for concision/expression :)

All of the above need method chaining, though:

https://en.wikipedia.org/wiki/Method_chaining

This may feel a bit nit-picky, but they haven't been added to ECMAScript yet. There are two features here: the optional chaining operator[1] and nullish coalescing operator[2]. Both are at stage 3 of the standardization process.

Changes to proposed features can still be made at stage 3 if critical issues come up. Otherwise, once implementors have sufficient experience with them (and tests & multiple implementations exist) the proposals will be eligible to move to stage 4, after which they'll be able to be included as part of the next ECMAScript spec.

Both proposals are progressing through the process very smoothly, but I wouldn't rely on them for anything critical just yet.

[1] https://github.com/tc39/proposal-optional-chaining

[2] https://github.com/tc39/proposal-nullish-coalescing

Thats a good point. It is probably rare for proposals to change after stage 3 which is why the TypeScript team does not implement a ECMAScript feature until it reaches Stage 3.

In this case, members of the TS team were actually championing these ES proposals :)