From d567b1c2dcab74d73ab2f16a0f629906038e0035 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Mon, 30 Sep 2024 12:17:30 -0400 Subject: [PATCH] TS 5.6 post --- 2024/09/30/ts-56/index.html | 239 ++++++++++++++++++++++++++++++++++++ all-posts/index.html | 9 ++ archives/index.html | 54 ++++---- atom.xml | 43 ++++--- index.html | 54 ++++---- 5 files changed, 331 insertions(+), 68 deletions(-) create mode 100644 2024/09/30/ts-56/index.html diff --git a/2024/09/30/ts-56/index.html b/2024/09/30/ts-56/index.html new file mode 100644 index 0000000..f124319 --- /dev/null +++ b/2024/09/30/ts-56/index.html @@ -0,0 +1,239 @@ + + + + + + Notes on TypeScript 5.6 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+
+
+

Notes on TypeScript 5.6

+
+
+
+
+ +
+
+
+
+
+ +
+
+

We TypeScript developers are a lucky bunch. While some languages (Python, JavaScript) are released annually, every three years (C++) or even less, we get four new versions of TypeScript every year. TypeScript 5.6 was released on September 9th, 2024. Let's take a look.

+ + +

New Features

Disallowed Nullish and Truthy Checks

TypeScript will now alert you to certain conditionals that are always true or false:

+
const value = {} || 'unreachable';
+ +

Because {} is truthy, the right-hand side of the || is dead code. It should either be removed or investigated, since it might indicate a logic error.

+

If your project is large and has been around for a while, this check is likely to turn up some strange-looking code. For example, I got a "this expression is always truthy" error on code that looked like this:

+
const val = { ...obj, prop: value } || {};
+ +

What's that || {} doing there? Running git blame revealed the story. The code originally looked like this:

+
const val = obj || {};
+ +

Then a subsequent change added prop: value to the object and didn't remove the fallback. In this case, it's fine to remove the || {} since using object spread on a null/undefined value is OK.

+

This new check is the single best reason to update to TS 5.6. I haven't seen a single false positive, and I've found lots of strange-looking code. This matches the TypeScript team's findings.

+

Iterator Helper Methods

In addition to finding new errors in your code, new TypeScript releases continue the ongoing process of implementing all stage 3 ECMAScript features.

+

TypeScript 5.6 now supports Iterator Helper methods like map and take. If you've ever used Python's itertools package, this will be familiar. The appeal of iterators is that you can apply a series of operations to an array, for example, without constructing all the intermediate arrays. This reduces memory usage and should improve cache efficiency and performance.

+

Because these are JavaScript runtime methods, you'll need to use a runtime that supports them. At the moment that's Node.js 22 (which should enter long-term support in October) and around 67% of browsers. Unless you can guarantee support in your environment, you may want to wait on these for a bit.

+

Strict Builtin Iterator Checks (and --strictBuiltinIteratorReturn)

TypeScript's any type is dangerous: not only does it disable type checking, it can also silently spread through your program. Chapter 5 of Effective TypeScript is all about taming the any type.

+

Perhaps the scariest source of any types is type declaration files (.d.ts). If you call a function and it's declared to return any, then any is what you get, even if the word "any" never appears in your source code. JSON.parse is a famous example of this:

+
const obj = JSON.parse('{"a": 2}');  // whoops, any type!
const b = obj.b; // no error!
+ +

(Matt Pocock's ts-reset fixes this and a few other well known issues.)

+

One subtle source of any came from direct use of an iterator's .next() method:

+
const letters = new Set(['a', 'b', 'c']);
const oneLetter = letters.values().next().value;
// ^? const oneLetter: any (TS 5.5)
// string | undefined (TS 5.6)
+ +

The type in TS 5.6 makes a lot of sense! If the Set were empty, oneLetter would be undefined. Otherwise it would be a string. (You can also check the done property to narrow the type.) While directly working with an iterator is rare (you should typically use for-of loops or the new iterator helpers), this is a welcome improvement because it eliminates a surprising source of any types.

+

So the real question is… why was this an any type in older versions of TypeScript? To understand why, the TypeScript blog gives this example:

+
function* abc123() {
yield "a";
yield "b";
yield "c";
return 123;
}

const iter = abc123();

iter.next(); // { value: "a", done: false }
iter.next(); // { value: "b", done: false }
iter.next(); // { value: "c", done: false }
iter.next(); // { value: 123, done: true }
+ +

A generator function (which returns an iterator) can both yield and return values. When it returns a value, that goes into the value property of the iterator's value.

+

TypeScript models this with two type parameters: Iterator<T, TReturn>. Most iterators don't return a special value when they're done, so TReturn is typically void (the return type of a function without a return statement).

+

When TypeScript first added support for iterators in 2016, they didn't distinguish T and TReturn. When they did split these types in 2019, they had to default TReturn to any to maintain backwards compatibility. The kicked the can down the road for years until this release, when they added a new flag, strictBuiltinIteratorReturn, to fix it. This is enabled with --strict, so you should get it right away.

+

A few more quick notes on this:

+
    +
  • The types around iterators, generators and async iterators are all pretty confusing. I hope to write a blog post about them at some point in the future.
  • +
  • If you don't have strictNullChecks enabled, you may see some strange errors around value having a type of string | void. The fix is to enable strictNullChecks!
  • +
  • This was a surprising source of any types that could spread in your code. To limit the damage from these sorts of anys, consider using typescript-eslint's no-unsafe-assignment, York Yao's type-coverage tool, or my brand-new Any X-Ray Vision VS Code extension.
  • +
+

The --noUncheckedSideEffectImports Option

I first noticed this issue when I was working on the second edition of Effective TypeScript. I claimed that this would be an error:

+
import 'non-existent-file.css';
+ +

… but it wasn't! This is a pretty strange TypeScript behavior. For these "side-effect imports," where you don't import any symbols, TypeScript will try to resolve the path to the module. If it can, it will type check the file that you import. But if it can't, it will just ignore the import entirely.

+

Now you can change this behavior with noUncheckedSideEffectImports. If you use CSS imports, you're likely to get tons of errors when you first enable this, one for every import. The solution that the release notes suggest is to add this line to a .d.ts file:

+
declare module '*.css' {}
+ +

But this feels a bit too lenient. It will catch a typo if you get the extension wrong (.cs instead of .css). But it won't check that you're importing a file that exists. I experimented with listing all my CSS files in a .d.ts file:

+
declare module 'css/file1.css' {}
declare module 'css/file2.css' {}
+ +

But this didn't seem to work at all. Relative imports of these files still produced type errors. So I think this feature still needs some work to be useful.

+

Region-Prioritized Diagnostics in Editors

Like most compilers, TypeScript is self-hosting: tsc is written in TypeScript. This is a good idea because it's a form of dogfooding. The idea is that, since the TS team works in TypeScript every day, they'll be acutely aware of all the same issues that face other TypeScript developers.

+

Sometimes, though, this can have strange consequences. I suspect that most developers who contribute to TypeScript had a chuckle when they saw Region-Prioritized Diagnostics in Editors in the TS 5.6 release notes. The idea is that, for very large TypeScript files, the editor can focus on just the part that you're editing, rather than checking the whole file.

+

Sounds like a nice performance win. So why did I find this funny? It's because it's so clearly targeted at just one file, TypeScript's 50,000+ line checker.ts. It's incredible to me that the TS team implemented this feature rather than breaking up that file, but there you go!

+

New Errors

Whenever a new version of TypeScript comes out, I like to run it over all my projects and the code samples in Effective TypeScript using literate-ts to look for new errors. There were a few of them, including some surprises.

+

Several errors came from the new checks I discussed earlier in this post, "this expression is always truthy" and .next() calls having stricter types. These were all true positives: they flagged code that was suspicious.

+

There were also two types of errors that came as surprises.

+

One was a change in circularity detection for a code sample in Effective TypeScript, in Item 33: Push Null Values to the Perimeter of Your Types:

+
function extent(nums: Iterable<number>) {
let minMax: [number, number] | null = null;
for (const num of nums) {
if (!minMax) {
minMax = [num, num];
} else {
const [oldMin, oldMax] = minMax;
// ~~~~~~ ~~~~~~
// 'oldMin' / 'oldMax' implicitly has type 'any' because it does not have a
// type annotation and is referenced directly or indirectly in its own
// initializer.
// (Error before TS 5.4, OK in TS 5.4, 5.5, error again in TS 5.6)
minMax = [Math.min(num, oldMin), Math.max(num, oldMax)];
}
}
return minMax;
}
+ +

In the first edition of Effective TypeScript, the same snippet avoided destructuring assignment in the else clause due to the circularity error:

+
result = [Math.min(num, result[0]), Math.max(num, result[1])];
+ +

I filed an issue about this in 2019 and was excited to see that it was fixed with TS 5.4, just in time for the book release. Unfortunately, the fix got reverted and we're back to the circularity error. So I'll need to update the book.

+

I also ran into an issue where the inferred type parameters for a generic function call changed. It boiled down to something like this:

+
declare const f: <P>(
fn: (props: P) => void,
init?: P,
) => P;

interface Props {
req: string;
opt?: string;
}

const props = f(
(p: Props) => '',
{ req: "" },
);

props.opt
// Error in TS 5.6, OK in earlier versions
+ +

I used every-ts to bisect this to #57909. This PR changed how type inference worked between covariant and contravariant parameters. If you see a surprising type change like this after updating to TS 5.6, this change might be the reason.

+

After reading some comments, this all seems pretty murky. There's often no clearly correct inference, just tradeoffs. Given that, I'm a bit surprised that TypeScript changed the existing behavior. Be on the lookout for this one!

+

Performance changes

New TypeScript releases have the potential to speed up or slow down compile times, but I was unable to measure any significant changes with this release.

+

Conclusions

While TS 5.6 isn't quite the blockbuster that TS 5.5 was, the new "this expression is always truthy" checks and the more precise iterator types make it a worthwhile upgrade.

+

It's sometimes said that software dependencies obey a "reverse triangle inequality:" it's easier to go from v1→v2→v3 than it is to go from v1→v3 directly. The idea is that you can fix a smaller set of issues at a time. There's not much reason to hold off on adopting TypeScript 5.6. Doing so now will make upgrading to 5.7 easier in a few months.

+

Speaking of which, keep an eye on that release! I'm hoping that it will include the proposed enforceReadonly flag.

+ +
+
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ + Effective TypeScript Book Cover + +

Effective TypeScript shows you not just how to use TypeScript but how to use it well. Now in its second edition, the book's 83 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 »

+ +
+
+
+
+ +
+ + + + diff --git a/all-posts/index.html b/all-posts/index.html index d153a8f..fbe8cce 100644 --- a/all-posts/index.html +++ b/all-posts/index.html @@ -84,6 +84,15 @@

2024

+
+
+ 2024-09-30 +
+ +
+
2024-08-30 diff --git a/archives/index.html b/archives/index.html index 7f39d61..e4de762 100644 --- a/archives/index.html +++ b/archives/index.html @@ -146,6 +146,28 @@

Recent Blog Posts

+
+
+

+ Notes on TypeScript 5.6 +

+ +
+ +
+ +
+ +

We TypeScript developers are a lucky bunch. While some languages (Python, JavaScript) are released annually, every three years (C++) or even less, we get four new versions of TypeScript every year. TypeScript 5.6 was released on September 9th, 2024. Let's take a look.

+ Continue reading » + +
+
+
+ +

@@ -355,29 +377,6 @@

-
-
-

- Don't Write Traditional Getter and Setter Methods in JavaScript and TypeScript -

- -
- -
- -
- -

Getter and setter methods (getFoo, setFoo) are common in Java and considered a best practice. But they're a code smell that's best avoided in JavaScript and TypeScript because the problem they solve in Java does not exist in JS/TS. This post looks at what that problem is and how you can solve it in TypeScript without imposing the boilerplate of getters and setters.

- - Continue reading » - -
-
-
- -
@@ -386,6 +385,15 @@

Older Posts

+ +
2023-11-29 diff --git a/atom.xml b/atom.xml index 73b8009..e2027e0 100644 --- a/atom.xml +++ b/atom.xml @@ -6,7 +6,7 @@ - 2024-08-30T13:28:01.642Z + 2024-09-30T16:17:09.833Z https://effectivetypescript.com/ @@ -16,6 +16,25 @@ Hexo + + Notes on TypeScript 5.6 + + https://effectivetypescript.com/2024/09/30/ts-56/ + 2024-09-30T16:15:00.000Z + 2024-09-30T16:17:09.833Z + + We TypeScript developers are a lucky bunch. While some languages (Python, JavaScript) are released annually, every three years (C++) or even less, we get four new versions of TypeScript every year. TypeScript 5.6 was released on September 9th, 2024. Let's take a look.

New Features

Disallowed Nullish and Truthy Checks

TypeScript will now alert you to certain conditionals that are always true or false:

const value = {} || 'unreachable';

Because {} is truthy, the right-hand side of the || is dead code. It should either be removed or investigated, since it might indicate a logic error.

If your project is large and has been around for a while, this check is likely to turn up some strange-looking code. For example, I got a "this expression is always truthy" error on code that looked like this:

const val = { ...obj, prop: value } || {};

What's that || {} doing there? Running git blame revealed the story. The code originally looked like this:

const val = obj || {};

Then a subsequent change added prop: value to the object and didn't remove the fallback. In this case, it's fine to remove the || {} since using object spread on a null/undefined value is OK.

This new check is the single best reason to update to TS 5.6. I haven't seen a single false positive, and I've found lots of strange-looking code. This matches the TypeScript team's findings.

Iterator Helper Methods

In addition to finding new errors in your code, new TypeScript releases continue the ongoing process of implementing all stage 3 ECMAScript features.

TypeScript 5.6 now supports Iterator Helper methods like map and take. If you've ever used Python's itertools package, this will be familiar. The appeal of iterators is that you can apply a series of operations to an array, for example, without constructing all the intermediate arrays. This reduces memory usage and should improve cache efficiency and performance.

Because these are JavaScript runtime methods, you'll need to use a runtime that supports them. At the moment that's Node.js 22 (which should enter long-term support in October) and around 67% of browsers. Unless you can guarantee support in your environment, you may want to wait on these for a bit.

Strict Builtin Iterator Checks (and --strictBuiltinIteratorReturn)

TypeScript's any type is dangerous: not only does it disable type checking, it can also silently spread through your program. Chapter 5 of Effective TypeScript is all about taming the any type.

Perhaps the scariest source of any types is type declaration files (.d.ts). If you call a function and it's declared to return any, then any is what you get, even if the word "any" never appears in your source code. JSON.parse is a famous example of this:

const obj = JSON.parse('{"a": 2}');  // whoops, any type!
const b = obj.b; // no error!

(Matt Pocock's ts-reset fixes this and a few other well known issues.)

One subtle source of any came from direct use of an iterator's .next() method:

const letters = new Set(['a', 'b', 'c']);
const oneLetter = letters.values().next().value;
// ^? const oneLetter: any (TS 5.5)
// string | undefined (TS 5.6)

The type in TS 5.6 makes a lot of sense! If the Set were empty, oneLetter would be undefined. Otherwise it would be a string. (You can also check the done property to narrow the type.) While directly working with an iterator is rare (you should typically use for-of loops or the new iterator helpers), this is a welcome improvement because it eliminates a surprising source of any types.

So the real question is… why was this an any type in older versions of TypeScript? To understand why, the TypeScript blog gives this example:

function* abc123() {
yield "a";
yield "b";
yield "c";
return 123;
}

const iter = abc123();

iter.next(); // { value: "a", done: false }
iter.next(); // { value: "b", done: false }
iter.next(); // { value: "c", done: false }
iter.next(); // { value: 123, done: true }

A generator function (which returns an iterator) can both yield and return values. When it returns a value, that goes into the value property of the iterator's value.

TypeScript models this with two type parameters: Iterator<T, TReturn>. Most iterators don't return a special value when they're done, so TReturn is typically void (the return type of a function without a return statement).

When TypeScript first added support for iterators in 2016, they didn't distinguish T and TReturn. When they did split these types in 2019, they had to default TReturn to any to maintain backwards compatibility. The kicked the can down the road for years until this release, when they added a new flag, strictBuiltinIteratorReturn, to fix it. This is enabled with --strict, so you should get it right away.

A few more quick notes on this:

  • The types around iterators, generators and async iterators are all pretty confusing. I hope to write a blog post about them at some point in the future.
  • If you don't have strictNullChecks enabled, you may see some strange errors around value having a type of string | void. The fix is to enable strictNullChecks!
  • This was a surprising source of any types that could spread in your code. To limit the damage from these sorts of anys, consider using typescript-eslint's no-unsafe-assignment, York Yao's type-coverage tool, or my brand-new Any X-Ray Vision VS Code extension.

The --noUncheckedSideEffectImports Option

I first noticed this issue when I was working on the second edition of Effective TypeScript. I claimed that this would be an error:

import 'non-existent-file.css';

… but it wasn't! This is a pretty strange TypeScript behavior. For these "side-effect imports," where you don't import any symbols, TypeScript will try to resolve the path to the module. If it can, it will type check the file that you import. But if it can't, it will just ignore the import entirely.

Now you can change this behavior with noUncheckedSideEffectImports. If you use CSS imports, you're likely to get tons of errors when you first enable this, one for every import. The solution that the release notes suggest is to add this line to a .d.ts file:

declare module '*.css' {}

But this feels a bit too lenient. It will catch a typo if you get the extension wrong (.cs instead of .css). But it won't check that you're importing a file that exists. I experimented with listing all my CSS files in a .d.ts file:

declare module 'css/file1.css' {}
declare module 'css/file2.css' {}

But this didn't seem to work at all. Relative imports of these files still produced type errors. So I think this feature still needs some work to be useful.

Region-Prioritized Diagnostics in Editors

Like most compilers, TypeScript is self-hosting: tsc is written in TypeScript. This is a good idea because it's a form of dogfooding. The idea is that, since the TS team works in TypeScript every day, they'll be acutely aware of all the same issues that face other TypeScript developers.

Sometimes, though, this can have strange consequences. I suspect that most developers who contribute to TypeScript had a chuckle when they saw Region-Prioritized Diagnostics in Editors in the TS 5.6 release notes. The idea is that, for very large TypeScript files, the editor can focus on just the part that you're editing, rather than checking the whole file.

Sounds like a nice performance win. So why did I find this funny? It's because it's so clearly targeted at just one file, TypeScript's 50,000+ line checker.ts. It's incredible to me that the TS team implemented this feature rather than breaking up that file, but there you go!

New Errors

Whenever a new version of TypeScript comes out, I like to run it over all my projects and the code samples in Effective TypeScript using literate-ts to look for new errors. There were a few of them, including some surprises.

Several errors came from the new checks I discussed earlier in this post, "this expression is always truthy" and .next() calls having stricter types. These were all true positives: they flagged code that was suspicious.

There were also two types of errors that came as surprises.

One was a change in circularity detection for a code sample in Effective TypeScript, in Item 33: Push Null Values to the Perimeter of Your Types:

function extent(nums: Iterable<number>) {
let minMax: [number, number] | null = null;
for (const num of nums) {
if (!minMax) {
minMax = [num, num];
} else {
const [oldMin, oldMax] = minMax;
// ~~~~~~ ~~~~~~
// 'oldMin' / 'oldMax' implicitly has type 'any' because it does not have a
// type annotation and is referenced directly or indirectly in its own
// initializer.
// (Error before TS 5.4, OK in TS 5.4, 5.5, error again in TS 5.6)
minMax = [Math.min(num, oldMin), Math.max(num, oldMax)];
}
}
return minMax;
}

In the first edition of Effective TypeScript, the same snippet avoided destructuring assignment in the else clause due to the circularity error:

result = [Math.min(num, result[0]), Math.max(num, result[1])];

I filed an issue about this in 2019 and was excited to see that it was fixed with TS 5.4, just in time for the book release. Unfortunately, the fix got reverted and we're back to the circularity error. So I'll need to update the book.

I also ran into an issue where the inferred type parameters for a generic function call changed. It boiled down to something like this:

declare const f: <P>(
fn: (props: P) => void,
init?: P,
) => P;

interface Props {
req: string;
opt?: string;
}

const props = f(
(p: Props) => '',
{ req: "" },
);

props.opt
// Error in TS 5.6, OK in earlier versions

I used every-ts to bisect this to #57909. This PR changed how type inference worked between covariant and contravariant parameters. If you see a surprising type change like this after updating to TS 5.6, this change might be the reason.

After reading some comments, this all seems pretty murky. There's often no clearly correct inference, just tradeoffs. Given that, I'm a bit surprised that TypeScript changed the existing behavior. Be on the lookout for this one!

Performance changes

New TypeScript releases have the potential to speed up or slow down compile times, but I was unable to measure any significant changes with this release.

Conclusions

While TS 5.6 isn't quite the blockbuster that TS 5.5 was, the new "this expression is always truthy" checks and the more precise iterator types make it a worthwhile upgrade.

It's sometimes said that software dependencies obey a "reverse triangle inequality:" it's easier to go from v1→v2→v3 than it is to go from v1→v3 directly. The idea is that you can fix a smaller set of issues at a time. There's not much reason to hold off on adopting TypeScript 5.6. Doing so now will make upgrading to 5.7 easier in a few months.

Speaking of which, keep an eye on that release! I'm hoping that it will include the proposed enforceReadonly flag.

]]>
+ + + + <p>We TypeScript developers are a lucky bunch. While some languages (<a href="https://en.wikipedia.org/wiki/History_of_Python">Python</a>, <a href="https://en.wikipedia.org/wiki/ECMAScript_version_history">JavaScript</a>) are released annually, every three years (<a href="https://en.wikipedia.org/wiki/C%2B%2B#Standardization">C++</a>) or even less, we get <em>four</em> new versions of TypeScript every year. TypeScript 5.6 was <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/">released</a> on September 9th, 2024. Let&#39;s take a look.</p> + + + + + +
+ A keyof puzzle @@ -65,7 +84,7 @@ https://effectivetypescript.com/2024/07/02/ts-55/ 2024-07-02T17:00:00.000Z - 2024-07-02T17:23:29.566Z + 2024-09-30T16:16:44.062Z We TypeScript developers are a lucky bunch. While some languages (Python, JavaScript) are released annually, every three years (C++) or even less, we get four new versions of TypeScript every year. TypeScript 5.5 was released on June 20th, 2024, and it was a real blockbuster. Let's take a look.

TypeScript's motto is "JavaScript with syntax for types." New versions of TypeScript don't add new runtime features (that's JavaScript's responsibility). Rather, they make changes within the type system. These tend to take a few forms:

  1. New ways of expressing and relating types (e.g., template literal types in TypeScript 4.1)
  2. Increased precision in type checking and inference
  3. Improvements to language services (e.g., a new quick fix)
  4. Support for new ECMAScript standards
  5. Performance improvements.

TypeScript 5.5 doesn't introduce any new type syntax, but it does include all the other kinds of changes. The official release notes have complete explanations and examples. What follows is my quick take on each of the major changes. After that we'll look at new errors and test some of the performance wins.

New Features

Inferred Type Predicates

This was my contribution to TypeScript, and I'm very happy that it's made it into an official release! I've written about it extensively on this blog before, so I won't go into too much more detail here:

Dimitri from Michigan TypeScript even recorded a video of me explaining the story of the feature to him and Josh Goldberg.

TypeScript will infer type predicates for any function where it's appropriate, but I think this will be most useful for arrow functions, the original motivator for this change:

const nums = [1, 2, 3, null];
// ^? const nums: (number | null)[]
const onlyNums = nums.filter(n => n !== null);
// ^? const onlyNums: number[]
// Was (number | null)[] before TS 5.5!

I have two follow-on PRs to expand this feature to functions with multiple return statements and to infer assertion predicates (e.g., (x: string): asserts x is string). I think these would both be nice wins, but it's unclear whether they have a future since the pain points they address are much less common.

Control Flow Narrowing for Constant Indexed Accesses

This is a nice example of improved precision in type checking. Here's the motivating example:

function f1(obj: Record<string, unknown>, key: string) {
if (typeof obj[key] === "string") {
// Now okay, previously was error
obj[key].toUpperCase();
}
}

Previously this would only work for constant property accesses like obj.prop. It's undeniable that this is a win in terms of precision in type checking, but I think I'll keep using the standard workaround: factoring out a variable.

function f1(obj: Record<string, unknown>, key: string) {
const val = obj[key];
if (typeof val === "string") {
val.toUpperCase(); // this has always worked!
}
}

This reduces duplication in the code and avoids a double lookup at runtime. It also gives you an opportunity to give the variable a meaningful name, which will make your code easier to read.

Where I can see myself appreciating this is in single expression arrow functions, where you can't introduce a variable:

keys.map(k => typeof obj[k] === 'string' ? Number(obj[k]) : obj[k])

Regular Expression Syntax Checking

Regular Expressions may be the most common domain-specific language in computing. Previous versions of TypeScript ignored everything inside a /regex/ literal, but now they'll be checked for a few types of errors:

  • syntax errors
  • invalid backreferences to invalid named and numbered captures
  • Using features that aren't available in your target ECMAScript version.

Regexes are notoriously cryptic and hard to debug (this online playground is handy), so anything TypeScript can do to improve the experience of writing them is appreciated.

Since the regular expressions in existing code bases have presumably been tested, you're most likely to run into the third error when you upgrade to TS 5.5. ES2018 added a bunch of new regex features like the /s modifier. If you're using them, but don't have your target set to ES2018 or later, you'll get an error. The fix is most likely to increase your target. The /s (dotAll) flag, in particular, is widely supported in browsers and has been available since Node.js since version 8 (2018). The general rule here is to create an accurate model of your environment, as described in Item 76 of Effective TypeScript.

Regex type checking is a welcome new frontier for TypeScript. I'm intrigued by the possibility that is a small step towards accurately typing the callback form of String.prototype.replace, JavaScript's most notoriously un-type friendly function:

"str".replace(/foo(bar)baz/, (match, capture) => capture);
// ^? (parameter) capture: any

Support for New ECMAScript Set Methods

When you have two sets, it's pretty natural to want to find the intersection, union and difference between them. It's always been surprising to me that JavaScript Sets didn't have this ability built in. Now they do!

While these new methods are stage 4, they haven't been included in any official ECMAScript release yet. (They'll probably be in ES2025.) That means that, to use them in TypeScript, you'll either need to set your target or lib to ESNext. Support for these methods is at around 80% in browsers at the moment, and requires Node.js 22 on the server, so use with caution or include a polyfill.

Isolated Declarations

The isolatedDeclarations setting opens a new front in the should you annotate your return types? debate. The primary motivator here is build speed for very large TypeScript projects. Adopting this feature won't give you a faster build, at least not yet. But it's a foundation for more things to come. If you'd like to understand this feature, I'd highly recommend watching Titian's TS Congress 2023 talk: Faster TypeScript builds with --isolatedDeclarations.

Should you enable this feature? Probably not, at least not yet. An exception might be if you use the explicit-function-return-type rule from typescript-eslint. In that case, switching to isolatedDeclarations will require explicit return type annotations only for your public API, where it's a clearer win.

I expect there will be lots more development around this feature in subsequent versions of TypeScript. I'll also just note that isolatedDeclarations had a funny merge conflict with inferred type predicates. All these new feature are developed independently, which makes it hard to anticipate the ways they'll interact together.

Performance and Size Optimizations

I sometimes like to ask: would you rather have a new feature or a performance win? In this case we get both!

Inferring type predicates does incur a performance hit. In some extreme cases it can be a significant one, but it's typically a 1-2% slowdown. The TypeScript team decided that this was a price they were willing to pay for the feature.

TypeScript 5.5 also includes a whole host of other performance improvements, though, so the net effect is a positive one. You get your feature and your performance, too.

Monomorphization has been an ongoing saga in TypeScript. This is a "death by a thousand cuts" sort of performance problem, which is hard to diagnose because it doesn't show up clearly in profiles. Monomorphization makes all property accesses on objects faster. Because there are so many of these, the net effect can be large.

One of the things we like about objects in JavaScript and TypeScript is that they don't have to fit into neat hierarchies like in Java or C#. But monomorphization is a push towards exactly these sorts of strict hierarchies. It's interesting to see this motivated by performance, rather than design considerations. If anyone tries to translate tsc to the JVM, say, this will help.

I was particularly happy with control flow graph simplifications, since the PR included a screenshot of the TS AST Viewer graph that I built!

These optimizations affect build times, the language service, and TypeScript API consumers. The TS team uses a set of benchmarks based on real projects, including TypeScript itself, to measure performance. I compared TypeScript 5.4 and 5.5 on a few of my own projects in a less scientifically rigorous way:

  • Verifying the 934 code samples in the second edition of Effective TypeScript using literate-ts, which uses the TypeScript API, went from 347→352s. So minimal change, or maybe a slight degradation.
  • Type checking times (tsc --noEmit) were unaffected across all the projects I checked.
  • The time to run webpack on a project that uses ts-loader went from ~43→42s, which is a ~2% speedup.

So no dramatic changes for my projects, but your mileage may vary. If you're seeing big improvements (or regressions), let me know! (If you're seeing regressions, you should probably file a bug against TypeScript.)

Miscellaneous

  • Editor and Watch-Mode Reliability Improvements: These are grungy quality of life improvements, and we should be grateful that the TypeScript team pays attention to them.
  • Easier API Consumption from ECMAScript Modules: I'd always wondered why you couldn't import "typescript" like other modules. Now you can!
  • Simplified Reference Directive Declaration Emit: A weird, dusty corner that no longer exists. Yay!

New Errors

Most of my TypeScript projects had no new errors after I updated to TS 5.5. The only exception was needing to update my target to ES2018 to get the /s regular expression flag, as described above.

Both Bloomberg and Google have posted GitHub issues describing the new errors they ran into while upgrading to TS 5.5. Neither of them ran into major issues.

Conclusions

Every new release of TypeScript is exciting, but the combination of new forms of type inference, isolatedDeclarations, and potential performance wins make this a particularly good one.

It's sometimes said that software dependencies obey a "reverse triangle inequality:" it's easier to go from v1→v2→v3 than it is to go from v1→v3 directly. The idea is that you can fix a smaller set of issues at a time. There's not much reason to hold off on adopting TypeScript 5.5. Doing so now will make upgrading to 5.6 easier in a few months.

]]>
@@ -399,26 +418,6 @@ This post and its accompanying video present six ways to solve this problem and -
- - - All I Want for Christmas Is… These Seven TypeScript Improvements - - https://effectivetypescript.com/2022/12/25/christmas/ - 2022-12-25T19:00:00.000Z - 2024-04-30T21:34:48.394Z - - 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.

]]>
- - - - 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? - - - - - -
diff --git a/index.html b/index.html index 7f39d61..e4de762 100644 --- a/index.html +++ b/index.html @@ -146,6 +146,28 @@

Recent Blog Posts

+
+
+

+ Notes on TypeScript 5.6 +

+ +
+ +
+ +
+ +

We TypeScript developers are a lucky bunch. While some languages (Python, JavaScript) are released annually, every three years (C++) or even less, we get four new versions of TypeScript every year. TypeScript 5.6 was released on September 9th, 2024. Let's take a look.

+ Continue reading » + +
+
+
+ +

@@ -355,29 +377,6 @@

-
-
-

- Don't Write Traditional Getter and Setter Methods in JavaScript and TypeScript -

- -
- -
- -
- -

Getter and setter methods (getFoo, setFoo) are common in Java and considered a best practice. But they're a code smell that's best avoided in JavaScript and TypeScript because the problem they solve in Java does not exist in JS/TS. This post looks at what that problem is and how you can solve it in TypeScript without imposing the boilerplate of getters and setters.

- - Continue reading » - -
-
-
- -
@@ -386,6 +385,15 @@

Older Posts

+ +
2023-11-29