惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
C
Cisco Blogs
Cloudbric
Cloudbric
The Last Watchdog
The Last Watchdog
L
LINUX DO - 热门话题
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Security Archives - TechRepublic
Security Archives - TechRepublic
TaoSecurity Blog
TaoSecurity Blog
V2EX - 技术
V2EX - 技术
H
Heimdal Security Blog
S
Security Affairs
L
Lohrmann on Cybersecurity
Hacker News - Newest:
Hacker News - Newest: "LLM"
Simon Willison's Weblog
Simon Willison's Weblog
WordPress大学
WordPress大学
小众软件
小众软件
Security Latest
Security Latest
AWS News Blog
AWS News Blog
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
Engineering at Meta
Engineering at Meta
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
F
Full Disclosure
S
Schneier on Security
L
LangChain Blog
MyScale Blog
MyScale Blog
Know Your Adversary
Know Your Adversary
P
Privacy International News Feed
Google Online Security Blog
Google Online Security Blog
Scott Helme
Scott Helme
Stack Overflow Blog
Stack Overflow Blog
爱范儿
爱范儿
A
Arctic Wolf
Martin Fowler
Martin Fowler
B
Blog RSS Feed
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
The Register - Security
The Register - Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园_首页
Latest news
Latest news
F
Fortinet All Blogs
G
GRAHAM CLULEY
T
The Exploit Database - CXSecurity.com
Hacker News: Ask HN
Hacker News: Ask HN

oida.dev | TypeScript, Rust

TypeScript's `erasableSyntaxOnly` Flag Unsafe for work Tokio: Macros Tokio: Channels Tokio: Getting Started Network Applications on the Tokio Stack Remake, Remodel, Reduce. 5 Inconvenient Truths about TypeScript Refactoring in Rust: Introducing Traits Refactoring in Rust: Abstraction with the Newtype Pattern Announcing the TypeScript Cookbook TypeScript: Iterating over objects The road to universal JavaScript 10 years of oida.dev Rust: Tiny little traits The TypeScript converging point How not to learn TypeScript Getting started with Rust Introducing Slides and Coverage TypeScript: The humble function overload TypeScript + React: Children types are broken TypeScript: In defense of any Rust: Enums to wrap multiple errors Dissecting Deno Error handling in Rust TypeScript: Unexpected intersections Upgrading Node.js dependencies after a yarn audit TypeScript: Array.includes on narrow types TypeScript + React: Typing Generic forwardRefs shared, util, core: Schroedinger's module names Learning Rust and Go TypeScript: Narrow types in catch clauses TypeScript: Low maintenance types Tidy TypeScript: Name your generics Tidy TypeScript: Avoid traditional OOP patterns Tidy TypeScript: Prefer type aliases over interfaces Tidy TypeScript: Prefer union types over enums My new book: TypeScript in 50 Lessons Go Preact! ❤️ this in JavaScript and TypeScript TypeScript and ECMAScript Modules TypeScript + React: Why I don't use React.FC TypeScript + React: Component patterns TypeScript: Augmenting global and lib.dom.d.ts Vite with Preact and TypeScript TypeScript: Union to intersection type 11ty: Generate Twitter cards automatically Are large node module dependencies an issue? TypeScript: Variadic Tuple Types Preview TypeScript: Improving Object.keys Remake, Remodel. Part 4. TypeScript + React: Typing custom hooks with tuple types TypeScript: Assertion signatures and Object.defineProperty TypeScript: Check for object properties and narrow down type Boolean in JavaScript and TypeScript void in JavaScript and TypeScript Symbols in JavaScript and TypeScript Why I use TypeScript TypeScript + React: Extending JSX Elements TypeScript: Validate mapped types and const context TypeScript: Match the exact object shape TypeScript: The constructor interface pattern Streaming your Meetup - Part 4: Directing and Streaming with OBS Streaming your Meetup - Part 3: Speaker audio Streaming your Meetup - Part 2: Speaker video Streaming your Meetup - Part 1: Basics and Projector TypeScript and React Guide: Added a new styles chapter TypeScript and React Guide: Added a new render props chapter TypeScript and React: Styles and CSS TypeScript and React TypeScript and React Guide: Added a new prop types chapter TypeScript without TypeScript -- JSDoc superpowers TypeScript: Mapped types for type maps JAMStack vs serverless web apps The Unsung Benefits of JAMStack Sites TypeScript: Ambient modules for Webpack loaders My most favourite talks in 2018 TypeScript and React Guide: Added a new context chapter TypeScript: Built-in generic types TypeScript: Type predicates JSX is syntactic sugar TypeScript and React Guide: Added a new hooks chapter Getting your CfP application right FAQ on our Angular Connect Talk: Automating UI development TypeScript and Substitutability Debugging Node.js apps in TypeScript with Visual Studio Code From Medium: Deconfusing Pre- and Post-processing From Medium: PostCSS misconceptions Saving and scraping a website with Puppeteer Cutting the mustard - 2018 edition Wordpress as CMS for your JAMStack sites My most favourite podcast episodes in 2017 My most favourite talks in 2017 My most favourite books in 2017 The Best Request Is No Request, Revisited Not so hidden figures - Organizing ScriptConf My podcast journey to ScriptCast Grid layout, grid layout everywhere! #scriptconf and #devone Object streams in Node.js
The `never` type and error handling in TypeScript
2024-03-05 · via oida.dev | TypeScript, Rust

One thing that I see more often recently is that folks find out about the never type, and start using it more often, especially trying to model error handling. But more often than not, they don’t use it properly or overlook some fundamental features of never. This can lead to faulty code that might act up in production, so I want to clear doubts and misconceptions, and show you what you can really do with never.

I’ve written my second book on TypeScript. Check out The TypeScript Cookbook on Amazon!

never and errors. #

First of all, don’t blame developers for misunderstandings. The docs promote an example of never and error handling that is true if looked at in isolation, but it’s not the whole story. The example is this:

// Function returning never must not have a reachable endpoint
function error(message: string): never {
throw new Error(message);
}

This comes from the old docs, which are deprecated. The new docs do a much better job, yet this example sticks around in lots of places and is referenced in many blog posts out there.

It’s Schrödinger’s example: It’s both correct and wrong until you open the box and use it in situations that are not as simple as the one in the example.

Let’s look at the correct version. The example states that a function returning never must not have a reachable endpoint. Cool, so if I call this function, the binding I create will be unusable, right?

function error(message: string): never {
throw new Error(message);
}

const a = error("What is happening?");
// ^? const a: never

Yes! The type of a is never, and I can’t do anything with it. What TypeScript checks for us is that this function won’t ever return a valid value. So it correctly approves that the never return type matches the error thrown.

But you rarely just break your code in a single function without some extra stuff going on. Usually, you have either a correct value or you throw something.

What I see people do is this:

function divide(a: number, b: number): number | never {
if (b == 0) {
throw new Error("Division by Zero!");
}
return a / b;
}

const result = divide(2, 0);

if (typeof result === "number") {
console.log("We have a value!");
} else {
console.log("We have an error!");
}

You want to model your function in a way that in the “good” case, you return a value of type number, and you want to indicate that this might return an error. So it’s number | never.

And this example is 100% bogus, wrong, and doesn’t express the truth at all! If you look at the type of result, you see that the type is only number. Where has never gone?

Again, I don’t blame the developers. If you look at the original example describing the never type, you might draw your conclusion that this is how you want to handle the error case.

But I do blame bloggers for creating cheap Medium articles that reach the top hit on Google with wrong information that they didn’t even bother to test. I won’t link the culprit to not give them any link juice, but it’s easy to find with the right keywords. Kids, don’t do this. All your LLMs will learn the wrong things. And your readers, too.

What happened to never? #

Alright, where did the never type go? It’s easy to understand if you know what never actually represents, and how it works in the type system.

The TypeScript type system represents types as sets of values. The type checker’s purpose is to make sure that a certain known value is part of a certain set of values. If you have a variable with the value 2, it will be part of the set of number. The type boolean allows for the values true and false. You can fine-grain your types and create unions making the set bigger, or intersections, making the set smaller.

I write at length about this in my book, TypeScript Cookbook, so if you want to learn more about this, you can check it out.

The never type also represents a set of values, the empty set. No value is compatible with never. It indicates a situation that should never happen. This is also known as a bottom type.

The reason why never disappeared is simple set theory. If you create a union of a set number and the empty set, well all that remains is number. If you add nothing to something, something remains, after all.

never gets swallowed up by reality, and you won’t be able to indicate that this function might return an error. The type system will just ignore it.

Take away the following: Don’t use never as a representation for a thrown Error.

How to correctly use never for error handling #

This doesn’t render never useless, though. There are situations where you can model impossible states with this type.

First, through the use of discriminated unions and exhaustiveness checks. I’ve spoken about this feature in my blog and also in my book, and here’s a quick rundown.

Think of expressing your models as a union type.

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Rectangle = { kind: "rectangle"; width: number; height: number };

type Shape = Circle | Square | Rectangle;

Note that I set the kind property to a literal type. This is a discriminated union. Usually, when creating a union type, TypeScript will allow elements that fall into the overlapping areas of the sets, meaning that an object with { radius: 3, side: 4, width: 5 } would be accepted as a Shape.

But by using a literal type, TypeScript can distinguish between the different types and will only allow the correct properties for each type. This is because "circle" | "square" | "rectangle" don’t have any overlap.

Also, note that we use a literal string here as a type. This is not a value. "circle" is a type that only accepts a single value, the literal string called "circle".

With this discriminated union, we can now use exhaustiveness checks to make sure that we handle all cases.

function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "square":
return s.side ** 2;
case "rectangle":
return s.width * s.height;
default:
// tbd
}
}

You even get nice autocomplete in your editor and TypeScript will tell you which cases to handle.

We haven’t handled the default case yet, but we can use never to indicate that this case should never happen.

function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}

function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "square":
return s.side ** 2;
case "rectangle":
return s.width * s.height;
default:
return assertNever(s);
}
}

This is interesting. We have a default case that should never happen because our types won’t allow it, and we use never as a parameter type. Meaning that we pass a value, even though the never set doesn’t have any values. Something is going incredibly wrong if we reach this stage!

And we can use this to let TypeScript help us in the case that our code should change. Let’s introduce a new variant of Shape without changing the area function.

type Triangle = { kind: "triangle"; a: number; b: number; c: number };

type Shape = Circle | Square | Rectangle | Triangle;

function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "square":
return s.side ** 2;
case "rectangle":
return s.width * s.height;
default:
return assertNever(s);
// ~
// Argument of type 'Triangle' is not assignable
// to parameter of type 'never'.
}
}

Look at that! TypeScript understands that we didn’t check all variants, and our code will throw red squigglies at us. Time to check if we did everything right!

This is the good stuff about never. It helps you make sure that all your values are handled, and if not, it will tell you through red squigglies.

Error types. #

You now know how never actually works, but you still want to have a way to correctly express errors.

There is a way that is inspired by functional programming languages and made popular by Rust. You can use a result type to express that a function might fail.

This is different from using Error classes, which are a topic on their own. This here gives you better error states through the use of discriminated unions and exhaustiveness checks.

We do the following:

  • We define a type Error that carries the error message and has a kind property set to "error".
  • We define a generic type Success that carries the value and has a kind property set to "success".
  • Both types are combined into a Result type, which is a union of Error and Success.
  • We define two functions error and success to create the respective types.

Like this:

type ErrorT = { kind: "error"; error: string };
type Success<T> = { kind: "success"; value: T };

type Result<T> = ErrorT | Success<T>;

function error(msg: string): ErrorT {
return { kind: "error", error: msg };
}

function success<T>(value: T): Success<T> {
return { kind: "success", value };
}

Let’s refactor the divide function from above to use this Result type.

function divide(a: number, b: number): Result<number> {
if (b === 0) {
return error("Division by zero");
}
return success(a / b);
}

If we want to use the result, we need to check for the kind property and handle the respective case.

const result = divide(10, 0);

if (result.kind === "error") {
// result is of type Error
console.error(result.error);
} else {
// result is of type Success<number>
console.log(result.value);
}

The important thing is that the types are correct, and the type system knows about all the possible states.

And you can play around with that. Maybe you have functions that throw errors. Create a safe function that takes the original function and its arguments, and wraps everything into your newly created error handling system.

function safe<Args extends unknown[], R>(
fn: (...args: Args) => R,
...args: Args
): Result<R> {
try {
return success(fn(...args));
} catch (e: any) {
return error("Error: " + e?.message ?? "unknown");
}
}

function unsafeDivide(a: number, b: number): number {
if (b == 0) {
throw new Error("Division by Zero!");
}
return a / b;
}

const result = safe(unsafeDivide, 10, 0);

Or if you have a Result, and you want to fail at some point, well then do so:

function fail<T>(fn: () => Result<T>): T {
const result = fn();
if (result.kind === "success") {
return result.value;
}
throw new Error(result.error);
}

const a = fail(divide(10, 0));

It’s not perfect, but you have clear states, clear types, know about what your sets can contain, and when you really have no possible value left.

Conclusion #

I found some code piece expressing thrown Errors with never a while ago and thought “Oh, the docs messed something up”. I got into a rabbit hole seeing that folks on Medium are suggesting this as a good practice. If there’s something that annoys me, it’s folks teaching things wrong. So I wrote this article to clear things up. I hope it does. I have more articles like this on this very blog, but you find 100+ recipes in my book TypeScript Cookbook.

Related Articles