TypeScript 2024: The Year in Review

4 min read

TypeScript and ECMAScript have seen remarkable progress in 2024, with several groundbreaking features enhancing developer productivity and type safety. Let’s explore the major developments that landed this year.

Preserved Narrowing in Closures

TypeScript 5.4 introduced smarter type narrowing in closures, particularly after last assignments:

function processURL(url: string | URL) {
    let urlObject: URL;
    
    if (typeof url === "string") {
        urlObject = new URL(url);
    } else {
        urlObject = url;
    }
    
    // TypeScript now correctly narrows the type in the callback
    return ["search", "hash"].map(prop => {
        return urlObject[prop]; // urlObject is correctly typed as URL
    });
}

NoInfer Utility Type

The new NoInfer utility type in TypeScript 5.4 gives developers more control over type inference:

function createPair<T>(x: T, y: NoInfer<T>) {
    return [x, y];
}
 
// Works fine - T is inferred as number
createPair(1, 2);
 
// Error - T is inferred as number from first argument
createPair(1, "hello");

Object and Map GroupBy

TypeScript 5.4 added support for the new ECMAScript Object.groupBy and Map.groupBy methods:

const items = [
    { type: "fruit", name: "apple" },
    { type: "vegetable", name: "carrot" },
    { type: "fruit", name: "banana" }
];
 
// Type-safe grouping with Object.groupBy
const groupedByType = Object.groupBy(items, item => item.type);
// Type: { [K: string]: { type: string, name: string }[] }
 
// Using Map.groupBy for more complex keys
const groupedMap = Map.groupBy(items, item => item.type);
// Type: Map<string, { type: string, name: string }[]>

Never-Initialized Variable Checks

TypeScript 5.7 introduced enhanced detection of variables that are never initialized, providing better safety against undefined behavior:

function processData() {
    let result: number;
    
    function printResult() {
        console.log(result); // Error: Variable 'result' is used before being assigned
    }
    
    // Forgot to initialize result
    printResult();
}

Path Rewriting for Relative Imports

TypeScript 5.7 added support for automatic path rewriting with the new --rewriteRelativeImportExtensions compiler option, improving compatibility with modern JavaScript runtimes:

// main.ts
import * as utils from "./utils.ts"; // TypeScript now handles .ts extensions correctly
 
// Compiled output (main.js)
import * as utils from "./utils.js"; // Automatically rewritten for runtime compatibility

Checked Import Attributes

TypeScript 5.4 improved type checking for import attributes, ensuring better safety when using module types:

// TypeScript validates the import attribute type
import data from "./data.json" with { type: "json" };
 
// Error: Type '"invalid"' is not assignable to type '"json"'
import wrong from "./data.json" with { type: "invalid" };

Auto-Import Support for Subpath Imports

TypeScript 5.4 enhanced auto-import functionality to better handle subpath imports:

// package.json
{
    "exports": {
        "./features": "./dist/features.js",
        "./utils": "./dist/utils.js"
    }
}
 
// TypeScript now suggests correct imports
import { someFeature } from "my-package/features";
import { someUtil } from "my-package/utils";

Record and Tuple Types

The Record and Tuple proposal reached Stage 3, bringing immutable data structures to JavaScript and TypeScript:

const point = #{ x: 10, y: 20 }; // Record type
const coordinates = #[1, 2, 3]; // Tuple type
 
// TypeScript understands their immutable nature
point.x = 30; // Error: Cannot modify readonly property
coordinates[0] = 5; // Error: Cannot modify readonly tuple
 
// Structural equality
#{ x: 1, y: 2 } === #{ x: 1, y: 2 } // true
#[1, 2, 3] === #[1, 2, 3] // true

Array.fromAsync

The Array.fromAsync proposal reached Stage 4 and is now part of the ECMAScript standard, with full TypeScript support:

// Convert async iterables to arrays
const asyncNumbers = async function*() {
  yield* [1, 2, 3];
};
 
const numbers = await Array.fromAsync(asyncNumbers());
// Type: Promise<number[]>
 
// Process async data streams
const responses = await Array.fromAsync(
  fetchPaginatedData(),
  async (response) => await response.json()
);

Symbol as WeakMap Keys

TypeScript now supports the Symbol as WeakMap keys proposal, enabling better memory management for symbol-based metadata:

const registry = new WeakMap<symbol, object>();
 
const uniqueSymbol = Symbol('metadata');
const data = { value: 42 };
 
registry.set(uniqueSymbol, data);
// Memory is freed when symbol is no longer referenced

Iterator Helpers

The Iterator Helpers proposal reached Stage 4, bringing functional programming patterns to iterators:

const numbers = [1, 2, 3, 4, 5].values();
 
const doubled = numbers
  .map(x => x * 2)
  .filter(x => x > 5)
  .take(2);
 
// TypeScript understands the resulting types
for (const num of doubled) {
  console.log(num); // 6, 8
}

Looking Forward

Several exciting features are in development for future releases:

Resources

Comments