Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
TypeScript 3.5 (devblogs.microsoft.com/typescript)
268 points by DanRosenwasser on May 29, 2019 | hide | past | favorite | 80 comments


I would really love to enjoy TypeScript, especially now that they've added HKTs, but I am constantly running into type errors when the types clearly match, leading to weird workaround code where the type of some key in some object is specified as:

false | 'x' | 'y' | undefined

Meaning passing in `'x'` should work just fine. But instead, it specializes it to just a string, then throws a type error. The workaround requires me to write the following value to avoid a type error:

'x' as 'x'

This is not the only time I've run into this kind of problem -- I run into this problem regularly -- this is just the simplest version of it that I've experienced. I just cannot take a type system seriously when workarounds like that are needed.

Add to that, in many cases being unable to import a JS library in that doesn't include typedefs, and I fail to see how TS can be considered a superset of JS. It's certainly sold that way, but it's clearly not totally true. TS+React requires --noImplicitAny, which cannot be worked around (it's labeled as being required due to a platform limitation).

I also find Microsoft's documentation of TypeScript to be very poor (and has no search feature -- I'm sure they have some convenient excuse for that). I tend to learn how things work from random Github comments or source code after reading the documentation over and over. What is `declare` used for in practice? They don't really say, they just say it's for declaring things, and give highly specific examples.

Maybe I'm the odd one here and maybe I'm missing some piece of information that a lot of TypeScript developers have, but I've yet to find it and given all the choice I have these days, there are numerous languages I would choose well before TypeScript.


> in many cases being unable to import a JS library in that doesn't include typedefs

Make sure you have "allowJs": true in your tsconfig.json. Otherwise, you'll have to create a minimal file with `declare module "xyz";`

> I fail to see how TS can be considered a superset of JS

You have to interpret all TS "errors" as warnings for that to be true (which they mostly are considering tsc still emits code regardless of non-syntax-error errors)

> I just cannot take a type system seriously when workarounds like that are needed.

Your problem really just seems to be that the type inference does not have magical powers. There is no other programming language that has union types as powerful as TypeScript (as in discriminating by whatever you want), but yeah when you do `return {x: 1, y: 2}` the compiler doesn't always know whether you mean to return an object where the type of x is `1` or where it is `number`. In some cases it could be better still, but doing this in general is just not possible.


> There is no other programming language that has union types as powerful as TypeScript

Ada's record [0] type stands out. From what I can see, TS' unions are not nearly as powerful.

[0] https://en.m.wikibooks.org/wiki/Ada_Programming/Types/record


> There is no other programming language that has union types as powerful as TypeScript

I suggest looking into Idris and other languages with dependent types. Nothing against typescript, but it’s not even close.


Idris doesn't have anonymous sum types, nor does Coq or Lean. Which language were you thinking of?


Ceylon comes to mind; haven't googled others.


> There is no other programming language that has union types as powerful as TypeScript

How about Swift and OCaml? ReasonML comes to mind too. Haven't tried actively comparing any of them though.


OCaml/Reason have discriminated unions, which means that you have to explicitly specify which variant of the union you're using, such as

  type union = Bool of bool | Char of char | Nothing

  let (x, y, z) = (Nothing, Bool true, Char 'x')


Is this not what enums with associated values are?


Probably... but in any case TypeScript also supports naked (edit: I just made up the name) unions, where you don't need to declare them upfront, and where values of different types can be included directly. Such as:

  let a = random() ? 'a' : 1
TypeScript infers type

  string | number


This is still a discriminated union, only that the discriminator is provided by Javascript and is thus hidden: it's `typeof`.


Yeah, that's the runtime perspective... but for the type system, they're different... with the "naked" union being much harder (as they're non-algebraic - `a | b | b` always equals `a | b` and might even equal just `a` for some values of `a` and `b`)


No, this works on any overlapping types too - even unsound combinations such as `T | Array<T>`


Interesting! Thanks!


What features are you thinking of in those languages? I don't know Swift but I can't think of anything comparable in OCaml.


Sorry; I don't have much of an answer here.

I merely know from experience that they have a pretty good type system, but not much more. So I asked :-)

Answers have been enlightening


This doesn't need magic powers. All you have to do is treat literal values inside type descriptions differently.


Inside of type descriptions, literal values are always treated as literals. The problem only appears when you declare a variable _without_ an explicit type like `const foo = {k: 'x'}`, which the compiler then infers to be of type `foo: {k: string}` and not `foo: {k: 'x'}`.

Both of these are valid inferences, and it's impossible to know which one the user wants in the general case, which is why you have to explicitly say `const foo = {k: 'x' as const}`.


So the example of `false | 'x' | 'y' | undefined` turning into string is invalid?

I feel like I'm missing something.


here's a full example. If you do this:

    function test(p: {k: false | 'x' | 'y' | undefined}) {
        // do stuff
    }
    const p = {k: 'x'} // inferred type: {k: string}
    //...
    test(p); // type error : Type 'string' is not assignable to type 'false | "x" | "y" | undefined'
    
The problem is that the type of p.k is inferred to `string` not `x`. The compiler can't know this is fine because you could call `foo(p)` in between the declaration and the `test` call, that mutates p.k.

If you add `as const` or an explicit type to p it is fine.

(I think this is what the GGP was referring to)


Didn't realize "as const" worked; will have to try that next time.


Thanks!


> I just cannot take a type system seriously when workarounds like that are needed.

Are you familiar with type theory and how easy it is for a type system to require too much work to be useful? Especially with conditional types. TypeScript has been doing a phenomenal job.

For this particular use case, since version 3.4 you can write `'x' as const` which will work no matter how large your type is.


Speaking of which, is there any tutorials on type theory that targets developers instead of mathematicians?


Hindley-Milner is a solid place to start. Here's an introduction: http://akgupta.ca/blog/2013/05/14/so-you-still-dont-understa...


> 'x' as 'x'

As others point out, 3.4 added `as const` which is more powerful and more generic.

> Add to that, in many cases being unable to import a JS library in that doesn't include typedefs, and I fail to see how TS can be considered a superset of JS. It's certainly sold that way, but it's clearly not totally true. TS+React requires --noImplicitAny, which cannot be worked around (it's labeled as being required due to a platform limitation).

No implicit any just restricts the type system from falling back to `any`, it doesn't stop you from explicitly using `any`. Most places where you get a noImplicitAny error you can add an `as any` or a `: any` assertion nearby. Explicit `any` is still often useful, even if only as a TODO marker for that day you can search for `any` in the codebase and try to remove it.

There's also `as unknown` or `: unknown` type assertions as another option for `any` in some case (`unknown` is stricter than `any` and implies you are going to be testing the value for runtime type information).

Use an explicit `any` applies to modules too. That problem with importing a JS library but getting a noImplicitAny error can be solved with an explicit `any`. That's where one particularly useful real world case of `declare` comes in. Add a file called something like `modules.d.ts` (the `.d.ts` is a convention that it won't have actual TS code, just declarations) and then to add explicit `any` for any module you want to import is as simple as:

    declare module "modulename";
`declare` is how you write type definitions for JS code. The simplest declaration is that a module exists and could be anything.

> I also find Microsoft's documentation of TypeScript to be very poor

There's a better documentation site being built with better search and with a lot more interactive examples. I don't know when it might be expected to be released, but BUILD talks seemed to indicate that Microsoft is aware that Typescript's documentation hasn't kept up with its release cadence and needs love.


> especially now that they've added HKTs

Hmm .. as far as I'm aware, they haven't added this yet: https://github.com/microsoft/TypeScript/issues/1213

Note that higher-kinded types (though I'd rather call it "higher-kinded polymorphism" or something—type-level functions are not really types) would allow code similar to this:

  function foo<F, A>(f: (v: A) => F<A>, v: A): F<A> {
      return f(v);
  }


My biggest hope is that one day they will make type error formatting way, way, way better in VSCode. There are some gnarly type errors that take a lot of concentration to work out from that tiny, unformatted error text box :|


"they've added HKT"

no they did not, if they did it would've been a major news.


TypeScript 3.4 has const assertions which solve the type widening issue you describe.


I second that, to a degree. I write mostly TypeScript and Rust these days and it still behaves unexpectedly sometimes.

Example: for a service we wanted to port Rusts’ Result.

https://gist.github.com/KenanSulayman/34a40daa3ebbd1e1bdf7c7...

Notice the ‘as any’ and ‘_T!: T;’ et al. hacks to make it work.

Other than that it’s completely a one to one port from the Rust core implementation.


Because you're trying to manually optimize code, which is kindof like saying 'I konw what I'm doing so TypeScript please shut up'. Take for example the `map_err` function of the `Ok` class. The function returns a different `Result`, but you're using the `… as any` trick to shut up the compiler. The correct implementation would be:

    map_err<U>(fn: (arg: E) => U): Result<T, U> {
        return new Ok<T, U>(this.value)
    }
I see why you're not doing that, because that allocates a new object, and that might seem bad. Even the implementation in Rust 'allocates' a new object, but then it relies on the rust compiler to optimize that out. Here's the relevant code:

    pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T,F> {
        match self {
            Ok(t) => Ok(t),
            Err(e) => Err(op(e))
        }
    }
See the `Ok(t) => Ok(t)` line? That's the same as `return new Ok(…)` in the TypeScript code. Unfortunately TypeScript is not an optimizing compiler, so it's less powerful than rust. You can either use tricks (`… as any`) and give up type safety or be truthful at the expense of performance.


To be clear, the option type in rust does not heap allocate at all; no need for the compiler to optimize things away.


Your problem there seems to mostly be that you tried to directly port it without any modifications, and your code is actually incorrect in some places. For example, if you implemented Result<T, E> as a single class that had a `if(this.is_ok()) ...; else ...` it would work fine without any type assertions or any anywhere.

As it is, Ok only depends on T and Err only depends on E, so why do you have them depend on both?

One problem is that TS does not want to merge unionized function signatures like this:

    map: (<U>(fn: (arg: "yep") => U) => Ok<U>) | (<U>(fn: (arg: any) => U) => Result<U, "nope">)
This can be mostly fixed by removing arguments that you don't need.

I couldn't figure out .map_or_else, but .map().or_else() works.

Admittedly, with this way you can get an output of Ok<a> | Ok<b> from some functions, but I don't know where that would be a problem (still fully type checked, you would just get the expected error further outside.

Here's a updated gist: https://gist.github.com/phiresky/621f8d8ed6eb00c9a0ddb47217e...


For the comment about not being able to use JS libs that don’t have typedefs, this is why I wish Facebook’s Flow was more popular. You can use as much or as little of typing as you want and don’t need to to write typedefs for every JavaScript lib you wish to use. It also had static code analysis that could infer types.


Flow won't infer types of arbitrary Javascript libraries. It will let you use them as untyped values, just like Typescript does when you set the allowJs option.

(I've used Flow for a long time. I'm starting to transition a large codebase from Flow to Typescript, and I'd highly recommend anyone to just start with Typescript rather than Flow. Flow's type inference isn't all that useful, and causes Flow to be magnitudes slower than Typescript. In a large codebase, it can take Flow tens of seconds to minutes to react to a code change and show you errors or auto-completions in your code after typing in an editor, compared to practically instantaneous updates with Typescript.)


TS can infer types in some cases (when the --allowJs option is on and the JS files are a part of the project, for instance), and it is on the roadmap to continue to make that stronger.

TS also doesn't need typedefs for every JS lib. It now defaults to assuming any module import is type `any` if it doesn't have typedefs and can't infer from the JS files for whatever reason.

In the case of the comment above the complication is using JS libs and also the compiler flag --noImplicitAny which causes an error on importing a module that defaults to type `any`. Making that an explicit `any` (with a `declare module "modulename";` in a .d.ts file) is all that is needed.


TS does not have HKT. The post simply described improved inference on types with several generic parameters.


Since this is the top comment, for anyone that got overly excited like I did - no, there are no HKTs in TypeScript yet unfortunately.


Try `as const`?


Wonderful! My favorites from this release:

- Omit helper type

- Smart Select (allows editor to expand/shrink selection based on syntactic construct)

- Extract to type alias (smart refactor of function parameters to their own type)

- Performance improvements


It sounds like type inference for object literals is coming along, even if there a few quibbles about some of the defaults on literals mentioned here.

Did they ever get a solution in place for partial function application (e.g. - as for Ramda.js)?

When this gets to the point where the only place I have to define explicit types is for JSON data read from the network, I’ll consider using it.

I know I’m in the minority, but I’d rather NOT have any type checking than have to read through Java-esque drivel (at least most modern languages put the types after the identifier like Pascal does, rather than before like C). In practice, I don’t spend much time chasing type errors, but do spend too much time reading through MEGO inducing verbiage which I would just as soon not.


I think it's best to require type annotations at interface boundaries, but not within implementations. That way, changes to the interface are obvious, but excessive verbosity within a module is limited.


I wish there was a better testing story for TypeScript. ts-jest is slow because it does not benefit from incremental compilation. Having tsc compile everything creates a dist/ folder with tests, fixtures, etc and causes all sorts of subtle issues to be worked around. Also, trying to use tsc-watch and something like jests watch runs into all sorts of weird race conditions though at least the build and the tests complete in about 1 second.

I looked into combining tsc-watch and jest, but jest does not currently support a public API. tsc-watch already emits an event on success which should be able to trigger (an already running!) test runner to incrementally test again.


Does anyone here have experience with slow TypeScript builds? I've updated to use --build and incremental, but it still takes 3s+ to build 30 files on a 8700k clocked at 5GHz.


Typically, TypeScript type-checks `lib.d.ts` every single compilation which can add at least half a second of build time. It will also do a full check of `.d.ts` files in your dependencies, which can also be pretty heavy.

We're considering ways we can avoid a full type check: https://github.com/microsoft/TypeScript/issues/31417

In the meantime, you can try out turning on the `skipLibCheck` compiler option to see if that helps at all, but be warned that it might not catch conflicts across multiple files.


Is the check of `lib.d.ts` not helped by --incremental?


Very specifically, 3.4 had a few perf regressions for some type signatures [1]. More generally, TS can be a bit slow (~seconds) especially for large webapp development if you need hot-reloading and such. There are tricks to emit without checking and doing async checking during the dev process which speeds things up quite a bit.

[1] https://github.com/Microsoft/TypeScript/issues/30663


I'm using 3.5.1, the issue is I don't have very many files and it's in a Node environment where it transpiles the file to the output directory with no bundling or hot reloading included.

What are some of these tricks to skip checking or do async checking? I did a quick search and came up with a GitHub issue [0] for adding the option to skip checking, but didn't see anything myself.

[0] https://github.com/Microsoft/TypeScript/issues/29651


Theres tooling for this for webpack described: https://github.com/TypeStrong/ts-loader#faster-builds

More generally though you have two loops one that does transpileOnly and one that doesn't. The latter is your type check process.


As I said there's no bundling, such as webpack.


Yes, the Typescript compiler is slow.

I tend to notice all compilers as feeling "too slow". I think the cause is that we all have wayyyy too much code. Not that we write, but when we use a library, that uses 10 libraries, that each use 10 libraries. Now you have a million lines of code, and all you wanted to do was print hello world. The compiler doesn't know until it's read all of that that you aren't using any of it. The solution is to depend on libraries more carefully.

As the go team says, a little copying is better than a little dependency. This is a motto that the npm community has NOT taken to heart. The cost is slow build times. (To be fair, the go community has not taken this to heart either.)


We do at Asana. I have not worked on it recently but I do believe that certain type signatures can have a large impact on build times.


TS 3.4 has known performance regressions. 3.5 helps, but it's still slower than 3.3.

I don't really notice it, though, because I've got `tsc --watch` running in a terminal continuously as I edit code.

When tsc sees edits, recompilation competes in tens of ms. It's really quick.


Have you ruled out node and disk access time? If 30-file builds are taking 3s I suspect a sizable chunk of that is overhead (i.e. I'd expect the same from node-sass on 30 .scss files).


Nope, strace reports 0.03s for all calls and about 0.02s of that is for filesystem stuff.


[flagged]


I can't downvote you but I want to point out that the only thing you accomplish with such a comment is coming off as ignorant and bitter.

I have no idea why on Earth you think the type system is broken. Objectively, it simply isn't, obviously, but I'd love to see you trip over your own feet trying to argue that it is.

Moreover, it's honestly remarkable how they've managed to retrofit such an amazing type system on top of an already existing language and have it feel so natural. It's truly an impressive feat of engineering.


I don't know if 3.5 fixes this, but here's an example from 3.4

    function messUpTheArray(arr: Array<string | number>): void {
      arr.push(3);
    }

    const strings: Array<string> = ['foo', 'bar'];
    messUpTheArray(strings);

    const s: string = strings[2];
    console.log(s.toLowerCase())
You obviously can't push a number into an Array<String>, yet typescript allows it.

For my money, ReasonML is a better solution for type issues (hindley-milner types are awesome).


Interesting, though it does flag the push() as an error if you use "Array<string> | Array<number>" as the argument type.

It almost seems like some sort of co/contravariance issue?

A function that reads from an Array<string | number> should be able to accept either an Array<string> or an Array<number> (or Array<string | number>), but a function that mutates it should only be able to accept Array<string | number>?


It seems like both projects have their own raisons d'être along with trade-offs.

TS is pretty well-integrated with JS ecosystem and supported by a large player, but has an imperfect type system; Reason is more “puristic” with pattern matching and all, but you have to keep your eyes peeled for impedance mismatch.

(The docs for Reason, or BuckleScript to be exact, have warnings like “If you come from JavaScript, a Reason object doesn't compile to a JS object” sprinkled here and there.)

I personally encountered the case you described a couple of times but generally using TS still saved me a lot of time I’d have wasted debugging, with minimal time spent on getting into the ecosystem.


> Objectively, it simply isn't, obviously

To be fair, TS has some pretty bad unsoundness problems, especially regarding co/contra variance in generics, and for example some annoying weirdness with number/string keys in objects. Still, the type system is one of the most amazing ones I know.


Yeah but if you think about it they are putting a straight jacket on a madman while a committee organizes regular PCP shipments to said madman.

That they got it to work amazes me, that they got it to work well astounds me.

It's one of the few parts of my stack that I interact with every day I don't occasionally want to hunt down the creators of.


> It's one of the few parts of my stack that I interact with every day I don't occasionally want to hunt down the creators of.

Reactive Extensions was definitely one of those pieces of stacks.


Could you help me specify why TS is a broken type system ? Thanks you very much.


They may have been talking about how TypeScript's type system is (intentionally) unsound. For example see https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-fun... and the notes on soundness in https://www.typescriptlang.org/docs/handbook/type-compatibil....


No kidding. I thought you were being facetious but reading the changelog it's clear that most of these would typically have been in a high-priority 3.4.1 release as a number of them are blocking (now there's code that compiled under 3.4 with nary a warning that won't compile under 3.5).


Right. Just read the changelog. It's literally regression fixes on compilation performance (which is introduced by typescript to begin with), and providing work arounds to do things typeless when new syntax feature XYZ is launched.

I'm getting a lot of flack on this thread but I stand by my judgement that working with typescript is slower and more frustrating for frontend work than standard JS.

My experience is just and anecdote, but I'd make a considered choice to pick typescript over minimal JS frontends. A large portion or this is based on my previous experience, so that is what it is.


The 2 big things missing on typescript:

1) Operator overloading

2) Runtime type checking of JSON payloads (on dev env at least)

Can't believe in 2019 every library has to create its own way to add 2 elements of the same class together e.g dataframe1.addTo(dataframe2) vs dataframe1 + dataframe2

And the most common error that it should help catch during development is that you get a JSON and you cast it but it means nothing cause on runtime the JSON can be something completely different and nothing breaks until is somewhere else hard to debug, I get that Microsoft don't want to add runtime overhead but it should be possible at least on development mode.


1) If your motivation is heavy mathematical programming then I hear you, but beware that you are the minority of the language's userbase, (more generally, most application-level programming languages' userbase), it's not something most people care about.

If you want to write clever DSLs and/or name methods in ways that save keystrokes like in scala, NO, please.

2) There are libraries such as runtypes and io-ts that integrate well with the static type system


> Runtime type checking of JSON payloads

We've been using Mobx State Tree for this, which also gives us frontend models and automatically generates interfaces. Obviously only works for you if you're using Mobx, but we love it.

https://github.com/mobxjs/mobx-state-tree


I use https://github.com/gcanti/io-ts for the runtime checking. It is similar to tcomb (same author)!

You can even define the type via io-ts and then make a TypeScript interface out of it!

Given this, and the other libraries that do something similar, I am not sure why Microsoft needs to add it to the language.


RE: JSON: I've started tagging my JSON blobs as `unknown` and then using type guards (typeof, instanceof, field access existence checks, Array.isArray checks, user defined type guards[1], etc.) my way to inferring the shape of that JSON blob is as expected, and it seems to work pretty well.

[1] https://www.typescriptlang.org/docs/handbook/advanced-types....


> 1) Operator overloading

Please, please, please, no.


You know it would be completely transparent right? Just gets transpiled into .__add__(obj)


We already saw this play out in Scala. People end up abusing these to create absolutely unreadable "DSL"s. Even when it's tastefully done, for example the "!" and "?" operator for messages in Akka, it's not better than the names "tell" and "ask".


Fair enough.


The explicit goal of TypeScript is to be JavaScript, with types — i.e. not introduce any features not likely to land in an ECMAScript spec.


Which is a shame because JavaScript is constrained due needing backwards-compatibility above all (among other issues) but TypeScript isn't


Not much of a shame IMO - there are plenty of other languages that transpile to JS without those goals, if you don't care about backwards compatability and 1:1 mappings to vanilla JS.

On the other hand, there's only one language that I'm aware of - TypeScript - prioritizing these things, which is a major selling point of it to me. I don't want yet another leaky abstraction, or yet another runtime heavy enough for those abstractions to not leak.

I just want enough types for my intellisense to work right, and for my sorry ass - spoiled by years of nothing but static typing systems - to be able to code without drowning in a veritable sea of uncaught type errors to debug, when I poke frontend stuff.


Typed and those features? there isn't a single one. But yeah, maybe what I need is a fork of TypeScript with those features (or maybe some sort of plugin)




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

Search: