Skip to main content

· 20 min read

Once in a while in TypeScript code, you may come across a comment that looks something like this:

ts
// @ts-expect-error
let value: number = "oops!";
ts
// @ts-expect-error
let value: number = "oops!";

You may also know that // @ts-expect-error suppresses type checking for a single line. Wanting to ignore type errors might seem counterintuitive at first. If a project already decided to use TypeScript, why disable type checking?

Rare cases do exist when TypeScript's type checking needs to be suppressed in a small area of code. TypeScript includes several "comment directives" that can change type checking behavior for a file or on a specific line. A comment directive is a comment that directs (changes) how a tool operates within a file.

This article will explain the kinds of comment directives supported by TypeScript: why they exist, what they do, and how to use them.

· 20 min read

TypeScript's type system allows any two types that seem to have the same structure to be assignable to each other. But what if we want to restrict a type to only allow certain values, even if other values happen to have the same structure? Say, marking a difference between sanitized and un-sanitized strings, or positive integers from all numbers?

This need is solvable with a pattern called "branded types", sometimes also called "opaque types". Let's dive into why one might want to use branded types, how to declare and use them, and some alternatives to the pattern.

· 20 min read

One long-requested feature for TypeScript is the ability for its types to describe what exceptions a function might throw. "Throw types", as the feature is often called, are used in some programming languages to help ensure developers call functions safely.

The popular strongly typed language Java, for example, implements throw types with a throws keyword. Without reading any other code, a developer would infer from the following first line of a positive function that the function is able to throw a ValueException:

java
public static void positive(int value) throws ValueException { /* ...*/ }
java
public static void positive(int value) throws ValueException { /* ...*/ }

Throw types are also useful for developer tooling. They can tell compilers when a function call might throw an exception without safe try/catch handling.

That all seems useful, so why doesn't TypeScript include throw types?

In short, doing so wouldn't be feasible for TypeScript -- and some would argue isn't practical in most programming languages. This blog post will dig into the benefits, drawbacks, and general blockers to including throw types in TypeScript. Let's dig in!

· 20 min read

TypeScript's provides several ways to describe the type of a function that can be called in multiple different ways. But the two most common strategies -function overloads and generic functions- don't help much with how the function's internal implementation understands the types of its parameters.

This article will walk through three techniques for describing a parameter type that changes based on a previous parameter. The first two allow using standard JavaScript syntax but aren't quite precise in describing the types inside the function. The third one requires using a ... array spread and [...] tuple type in a funky new way that gets the right types internally.

· 20 min read

Most projects in the JavaScript/TypeScript ecosystem release new versions with numbers respecting semantic versioning, or semver for short. Semver is a specification that describes how to predictably increase a package's version numbers upon each new release. TypeScript is notable for not following a strict interpretation of semver for its releases. This article will dig into:

  1. What semver is and why it's useful for many packages
  2. Why following a strict interpretation of semver would be impractical for TypeScript
  3. How TypeScript's releases are versioned to an interpretation of semver that makes sense for it

While TypeScript's diverging from a common community specification can be irksome for developers, there are real reasons why it chose to diverge.

The reasoning can be summarized as:

  • Nuances of TypeScript's type checking change in virtually every release
  • It would be impractical to increase TypeScript's major version for every type checking change
  • If we consider those type checking nuances as details rather than the public API, TypeScript actually has quite good adherance to semantic versioning

This article will also more deeply explain each of those points.

· 20 min read

TypeScript is a highly configurable language. It comes with over a hundred compiler options that can be provided via the command-line to tsc and/or in a "TSConfig" configuration file (by default, tsconfig.json).

tip

TypeScript's compiler options are documented at aka.ms/tsconfig.

compilerOptions.target in particular can be an important configuration option for your project. It specifies which ECMAScript version your project's output JavaScript code must support.

You can specify target in your TSConfig as the string name of an ECMAScript version, such as "es5"or"es2021":

jsonc
// tsconfig.json
{
"compilerOptions": {
"target": "es2021"
}
}
jsonc
// tsconfig.json
{
"compilerOptions": {
"target": "es2021"
}
}

This article explores what target influences and why that's useful. Let's dig in!

· 15 min read

TypeScript 4.9 introduces a new operator, satisfies, that allows opting into a different kind of type inference from the type system's default. satisfies brings the best of type annotations and default type inference together in a useful manner. Let's explore the new satisfies operator and why it's useful!

· 20 min read

TypeScript's type narrowing is a powerful feature of TypeScript's type system that lets it infer more specific types for values in areas of code. For example, TypeScript would understand that inside the following if statement, the fruit variable has to be the literal value "apple":

ts
const fruit = Math.random() > 0.5 ? "apple" : undefined;
 
fruit;
const fruit: "apple" | undefined
 
if (fruit) {
fruit;
const fruit: "apple"
}
ts
const fruit = Math.random() > 0.5 ? "apple" : undefined;
 
fruit;
const fruit: "apple" | undefined
 
if (fruit) {
fruit;
const fruit: "apple"
}

But, TypeScript's type system isn't perfect. There are some cases where TypeScript can't narrow types exactly the way you might want.

Take a look at this code snippet, where counts.apple is inferred to be type number:

ts
const counts = {
apple: 1,
};
 
counts.apple;
(property) apple: number
ts
const counts = {
apple: 1,
};
 
counts.apple;
(property) apple: number

While counts is type { apple: number }, shouldn't TypeScript know that the immediately available value of counts.apple is specifically the literal type 1 and not the general primitive type number? Can't TypeScript tell we haven't changed the value yet?

It could, but it won't. And for very good reason.

· 20 min read

TypeScript's type system is Turing Complete: meaning it has conditional branching (conditional types) and works with an arbitrary huge amount of memory. As a result, you can use the type system as its own programming language complete with variables, functions, and recursion. Developers have pushed the bounds of type operations possible in the type system to write some pretty incredible things!

This blog post is a starting list of nifty things TypeScript developers have pushed the type system to be able to do. They range from binary arithmetic and rudimentary virtual machines to maze solvers and full programming languages.

✋ This Is Not Normal

Most applications try to get away with as few extreme type operations as possible. Complex logic in the type system gets unreadable and hard to debug pretty quickly. The Learning TypeScript book advises:

If you do find a need to use type operations, please—for the sake of any developer who has to read your code, including a future you—try to keep them to a minimum if possible. Use readable names that help readers understand the code as they read it. Leave descriptive comments for anything you think future readers might struggle with.

Please don't look at the following projects list and think you need to understand them to use TypeScript. These projects are ridiculous. They're the equivalent of code golf: a fun activity for a select few, but not useful for most day-to-day work.

· 5 min read

Most popular programming languages use the void keyword to indicate that a function cannot return a value. Learning TypeScript describes TypeScript's void keyword as indicating that the returned value from a function will be ignored. Those two definitions are not always the same! TypeScript's void comes with an initially surprising behavior: a function type with a non-void return is considered assignable to a function type with a void return.

ts
let returnsVoid: () => void;
 
returnsVoid = () => "this is fine";
ts
let returnsVoid: () => void;
 
returnsVoid = () => "this is fine";

Why is that?