All I Want for Christmas Is… These Seven TypeScript Improvements

Christmas tree with presents It's Christmastime and I've been happily working through this year's Advent of Code in Deno (look forward to a blog post in the new year). What with all the presents, it's a good time to think about what we'd most like to see from TypeScript in the new year. Here are my top seven feature requests for 2023. Yes, that's a lot, but really I'd be thrilled with just one or two. Pretty please?

A faster language service

When you install TypeScript, you get two executables:

  • tsc, which checks your code for type errors and converts it to executable JavaScript
  • tsserver, which provides language services for your editor.

(This is discussed in Item 6 of Effective TypeScript: Use Your Editor to Interrogate and Explore the Type System.)

The faster these two programs can do their job, the happier you'll be as a developer. The TypeScript team is acutely aware of this: the release notes for new versions of TypeScript always talk about performance improvements in addition to new language features. The sluggishness of tsc remains a pain point for many developers, though. One of them even got so frustrated that he decided to rewrite tsc in Rust!.

Personally, I don't care much about the performance of tsc. I only tend to run as part of a continuous integration service or in "watch" mode without type checking via webpack or ts-node. The performance there is good enough for me.

What I do care about is the performance of tsserver. When you apply a refactor or change a type and have to wait for the red squiggly lines to catch up, that's tsserver being slow. Here's a GIF showing the language service having trouble keeping up:

A type error appearing and disappearing slowly after changing an import

These performance issues impact your moment-to-moment experience of TypeScript: did that red squiggle go away because I fixed the error, or because I'm waiting for tsserver to catch up? They're also hard to isolate for a bug report. If tsc is slow, I can point the TS team at my repo and report how long tsc takes to run. But to reproduce language server issues, you have to open a repo in your editor and then perform a particular action. It's not automated. And performance is inconsistent since it depends on caching.

So for 2023, I'd love to see a faster tsserver. Maybe we should rewrite that in Rust, too!

A typed pipe

When you compose several functions:

f(g(h(x)))

the functions are run in the right-to-left order: first h then g then f. This is counter to how code typically executes: top to bottom, left to right.

The pipeline proposal aims to offer a more readable alternative by introducing a new operator, |>:

x
|> h
|> g
|> f

The proposal page has lots of great material about why this is a good idea and is well worth reading. Unfortunately, though, there are two competing operator proposals and I don't anticipate this making it into JavaScript (and hence TypeScript) anytime soon. Axel Rauschmayer's blog has a good writeup on the current state of things.

There's an alternative, though: we can implement a function (commonly called pipe, or flow in lodash) that composes the functions in the order we expect:

const square = (n: number) => n ** 2;
const add1 = (n: number) => n + 1;
const halve = (n: number) => n / 2;
const f = pipe(square, add1, halve, String);
// ^? (arg: number) => string
const x = f(2); // "2.5"

Here square is applied first, then add1, then halve and finally String to convert the number to a string.

This solves the pipelining problem nicely but it has a problem: it's impossible to type. For details, see this Anders comment. The issue is that there needs to be a relationship between each of the arguments to pipe: the parameter type of each argument needs to match the return type of the previous one. And this just can't be modeled with TS.

The lodash and Ramda typings resort to the classic "death by a thousand overloads" solution: define safe versions for a small number of arguments (seven in lodash's case, ten in Ramda's) and give up on typing larger invocations.

This probably works fine in 99% of cases, but it doesn't feel right! I'd love to see the TypeScript type system expand to be able to type pipe, or see some form of the pipeline operator proposal adopted.

Records and Tuples

I'm cheating here since this is more of a JavaScript Christmas wish. But JS is TS, right? The Records and Tuples proposal, currently at Stage 2, seeks to add two new data structures to JavaScript. As the proposal puts it:

This proposal introduces two new deeply immutable data structures to JavaScript:

  • Record, a deeply immutable Object-like structure #{ x: 1, y: 2 }
  • Tuple, a deeply immutable Array-like structure #[1, 2, 3, 4]

TypeScript already has a notion of tuple types ([number, number]). This proposal would add tuple values, which would neatly resolve a number of ambiguities in type inference.

For example, if you write:

const pt = [1, 2]

then what should the type of pt be? It could be:

  • a tuple type ([number, number])
  • a readonly type (readonly [number, number])
  • a mutable list (number[])
  • an immutable list (readonly number[])

Without more information, TypeScript has to guess. In this case it infers the mutable list, number[]. You can use a const assertion (as const) to get (readonly [number, number]) or a typed identity function to get one of the others.

With this proposal, you'd write:

const pt = #[1, 2];

and it would be unambiguous that you want a tuple type. This is just the tip of the iceberg: functional programming and static typing work much better when you don't have to worry about mutability (see Item 27 of Effective TypeScript: Use Functional Constructs and Libraries to Help Types Flow).

The other great thing about this proposal is that we'd be able to use === to do structural comparisons between tuples and records:

> [1, 2] === [1, 2]
false
> #[1, 2] === #[1, 2]
true

The first comparison is false because the two arrays aren't the same object. Tuples have a more intuitive behavior. There is some risk of the Array / Tuple distinction being confusing, but Python has this and generally it works great.

We'd also be able to use tuples as keys in Set and Map structures. This is top of mind because tuples would have been wildly useful in the Advent of Code this year (see my 2020 post about using tuples as dict keys in Python).

Optional generics

While building the crosswalk and crudely-typed libraries, I frequently ran into this situation: you have a function that takes several generic arguments, you want the user to provide one of them explicitly, but you want TypeScript to infer the others.

Here's an example of what this would like:

function makeLookup<T, K extends keyof T>(k: K): (obj: T) => T[K] {
return (obj: T) => obj[k];
}

interface Student {
name: string;
age: number;
}

const lookupName = makeLookup<Student>('name');
// ^? const lookupName: (obj: Student) => string;
const lookupAge = makeLookup<Student>('age');
// ^? const lookupAge: (obj: Student) => number;

TypeScript doesn't let you do this. If you try it on the TypeScript playground you'll get this error: "Expected 2 type arguments, but got 1." Generics are all or nothing.

I wrote about two workarounds back in 2020: Use Classes and Currying to create new inference sites. But these are workarounds. I'd really love to have a way to do this without having to change my API!

The canonical issue for this feature request is #10571. There was some work on it in 2018 and I put up a proposal two years ago, but it hasn't seen much attention recently.

"Evolving" function types

TypeScript typically does a great job of inferring function parameter types from whatever context it has:

const squares = [1, 2, 3].map(x => x ** 2);
// ^? (parameter) x: number

The key point here is that you don't need to write (x: number) => x ** 2: TypeScript is able to infer that x is of type number from the types of [1, 2, 3] and the type of Array.prototype.map.

Now try factoring out a square function:

const square = x => x ** 2;
// Parameter 'x' implicitly has an 'any' type. (7006)
const squares = [1, 2, 3].map(square);

What worked so well in the first example completely fails here. This code is correct and is a simple refactor of the other code, but TypeScript demands a type annotation here. This is a frequent source of frustration in React components, where factoring out a callback can require writing out some very complex types. I wrote a blog post about this in 2019: How TypeScript breaks referential transparency …and what to do about it.

Why doesn't TypeScript infer the type of square (and hence x) from its usage on the next line? Anders is famously skeptical of "spooky action at a distance" where changing code in one place can cause a type to change and produce errors in other places that aren't obviously related.

But it does have one limited form of this: "evolving any", which is discussed in Effective TypeScript Item 41: Understand Evolving any. The gist is that TypeScript will sometimes let the type of a symbol change based on subsequent usage:

const out = [];
out.push(1);
out.push(2);
out
// ^? const out: number[]

I have a three year old proposal to expand this behavior to local function variables and make the square example valid. React developers around the world don't know that they want this feature for Christmas, but they do!

ES Module clarity

The JavaScript world is finally moving to ES modules (import and export). I've been blissfully ignoring some of the changes that Node.js and TypeScript have been making to support them, but I get the sense that this is an awkward transition for both of them. Hopefully we'll be through this by the end of 2023!

A canonical types → runtime path

One of the keys to really understanding TypeScript is recognizing that TypeScript types don't exist at runtime. They are erased. This is so fundamental that it's Item 1 in Effective TypeScript ("Understand the Relationship Between TypeScript and JavaScript").

But sometimes you really do want access to your TypeScript types at runtime, perhaps to do validation on untrusted inputs. There's a proliferation of libraries that let you define types in JavaScript and derive TypeScript types from them: zod, yup, io-ts and React PropTypes are just a few. Here's how you'd define a Student type with Zod, for example:

const Student = z.object({
name: z.string(),
age: z.number(),
});

type Student = z.infer<typeof Student>;
// type Student = { name: string; age: number; }

The advantage of defining a type in this way (rather than with a TypeScript interface) is that you can do runtime validation using the Student value (which you cannot do with the Student type):

const missingAge = Student.parse({name: "Bobby"});
// throws an error at runtime.

I prefer a different approach, though. TypeScript already has a great language for defining types and the relationships between them. Why learn another one? In crosswalk, I use typescript-json-schema to generate JSON Schema from my TypeScript type declarations. This JSON Schema is used to validate requests and generate Swagger/OpenAPI documentation.

But again, all these approaches are workarounds for the root issue: there's no way to get access to a TypeScript type at runtime. I'd love it if there were a canonical solution to this problem, so that we could all use the same solution. Perhaps decorators can help.

This would be a big change for TypeScript, and would generally go against its design philosophy. So while I have some hope for my other wishes, I have very little hope for this last one.


Would you be excited about any of these changes? What's on the top of your TypeScript Christmas list? Let me know in the comments or on Twitter.

Like this post? Consider subscribing to my newsletter, the RSS feed, or following me on Twitter.
Effective TypeScript Book Cover

Effective TypeScript shows you not just how to use TypeScript but how to use it well. The book's 62 items help you build mental models of how TypeScript and its ecosystem work, make you aware of pitfalls and traps to avoid, and guide you toward using TypeScript’s many capabilities in the most effective ways possible. Regardless of your level of TypeScript experience, you can learn something from this book.

After reading Effective TypeScript, your relationship with the type system will be the most productive it's ever been! Learn more »