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

推荐订阅源

T
Threatpost
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
D
DataBreaches.Net
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
Netflix TechBlog - Medium
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog
B
Blog
U
Unit 42
有赞技术团队
有赞技术团队
博客园 - 聂微东
GbyAI
GbyAI
宝玉的分享
宝玉的分享
F
Full Disclosure
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
MyScale Blog
MyScale Blog
Jina AI
Jina AI
Martin Fowler
Martin Fowler
IT之家
IT之家
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Docker
P
Proofpoint News Feed
A
About on SuperTechFans
I
InfoQ
博客园 - 【当耐特】
C
Check Point Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
Y
Y Combinator Blog
Project Zero
Project Zero
WordPress大学
WordPress大学
小众软件
小众软件
AWS News Blog
AWS News Blog
博客园 - 司徒正美
T
The Exploit Database - CXSecurity.com
L
LINUX DO - 热门话题
I
Intezer
Engineering at Meta
Engineering at Meta
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks
T
Tenable Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes

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. The `never` type and error handling in TypeScript 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 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
TypeScript: Unexpected intersections
2021-07-21 · via oida.dev | TypeScript, Rust

Sometimes when writing TypeScript, some of the things you’d usually do in JavaScript work a little different and cause some weird, and puzzling situations. Sometimes you just want to assign a value to an object property and get a weird error like “Type ‘string | number’ is not assignable to type ‘never’. Type ‘string’ is not assignable to type ‘never’.(2322)”

Don’t worry, this isn’t something out of the ordinary, it’s just something where “unexpected intersection types” make you think a little bit more about the type system.

Index access types and assignments #

Let’s look at this example:

let person = {
name: "Stefan",
age: 39
}

type Person = typeof person;

let anotherPerson: Person = {
name: "Not Stefan",
age: 20
};

function update(key: keyof Person) {
person[key] = anotherPerson[key]; // 💥
}

update("age");

We create a little function that lets us update things from one object anotherPerson to object person via providing a key. Both person and anotherPerson have the same type Person, but TypeScript throws error 2322 at us: Type ‘string | number’ is not assignable to type ‘never’. Type ‘string’ is not assignable to type ‘never’..

So what’s the deal?

Property assignments via the index access operator are super hard to track down for TypeScript. Even if you narrow down all possible access keys via keyof Person, the possible values that can be assigned are string or number (for name and age respectively). While this is ok if you have an index access on the right-hand side of a statement (reading), it gets a little interesting if you have an index access on the left-hand side of a statement (writing).

TypeScript can’t guarantee, that the value you pass along is actually correct. Look at this function signature:

function update_ambiguous(key: keyof Person, value: Person[keyof Person]) {
//...
}

update_ambiguous("age", "Stefan");

Nothing prevents me from adding a falsely typed value to every key. Except for TypeScript, which throws an error at us. But why does TypeScript tell us the type is never?

To allow for some assignments TypeScript compromises. Instead of not allowing any assignments at all on the right-hand side, TypeScript looks for the lowest common denominator of possible values. Take this for example:

type Switch = {
address: number,
on: 0 | 1
}

declare const switcher: Switch;
declare const key: keyof Switch;

Here, both keys are subsets of number. Well, address is the entire set of numbers, on on the other side is either 0 or 1. It’s absolutely possible to set 0 or 1 to both fields! And this is what you get with TypeScript as well.

switcher[key] = 1; //👍
switcher[key] = 2; //💥 Nope!

TypeScript gets to the possible assignable values by doing an intersection type of all property types. This means that in the case of the Switch, it’s number & (0 | 1), which boils down to 0 | 1. In the case of all Person properties, it’s string & number, which has no overlap, therefore it’s never! Hah! There’s the culprit!

So what can you do about it?

One way to get around this strictness (which is for your own good!) is by using generics. Instead of allowing all keyof Person values to access, we bind a specific subset of keyof Person to a generic variable:

function update<K extends keyof Person>(key: K) {
person[key] = anotherPerson[key]; // 👍
}

update("age");

When I do update("age"), K is bound to the literal type of "age". No ambiguity there!

There is a theoretical loophole since we could instantiate update with a much broader generic value:

update<"age" | "name">("age")

But this is something the TypeScript team allows… for now. See also this comment by Anders Hejlsberg. Note that Anders asks to see use cases for such a scenario, which perfectly details how the TypeScript team works. The original assignment via index access on the right-hand side has so much potential for errors, that they give you enough safeguards until you make it very intentional what you want to do. This is ruling out entire classes of errors without getting too much in the way.

Ambiguous functions #

There is another scenario where you experience unexpected intersection types. Take this wonderful discriminated union type for example:

type Singular = {
value: string,
validate: (val: string) => boolean,
kind: "singular"
}

type Multiple = {
value: string[],
validate: (val: string[]) => boolean,
kind: "multiple"
}

type Props = Singular | Multiple

Classy. Some very similar types with a nice literal type to create a distinction. But when we start using this in a function, things suddenly break:

function validate({ validate, value, kind }: Props) {
if (kind === "singular") {
validate(value); // 💥 Oh no!
}
}

The error that TypeScript throws at us is similar to the previous error, we get Error 2345: Argument of type ‘string | string[]’ is not assignable to parameter of type ‘string & string[]’.

Okay, so where does the intersection type string & string[] come from? The problem lies in the destructuring of our input arguments. The moment we destructure validate, value and kind out of our Props, they lose connection to the original type. Suddenly, we have three different types to deal with:

  • kind of type "singular" | "multiple"
  • value of type string | string[]
  • validate of type (val: string) => boolean | (val: string[]) => boolean

Again, no connection to the original type Props. So the moment we check for "singular", we are not hopping into another branch of the type system. This means that at the time we call validate, TypeScript thinks that it can be either one of both function types. It tries to create the lowest common denominator of all possible function types by creating an intersection type of all arguments of all functions.

So for the function to type-safely work, you would have to pass in a value of type string & string[]. Which is again very rare, actually impossible to have, some would say this can never happen.

So what can you do?

The answer is quite simple: Don’t destructure. In this case, it’s much easier to keep the original type relation intact.

function validate(props: Props) {
if(props.kind === "singular") {
props.validate(props.value);
}
}

TypeScript now knows exactly where to branch, and what types your object’s properties get.

The shocking final: A combination! #

It can get even harder 😱

Let’s look at the following structure:

type FormFields = {
age: {
value: number,
validator: (val: number) => boolean
},
name: {
value: string,
validator: (val: string) => boolean
}
}

You might already know where I’m getting at. What if I want to access a certain property via index access (a key), and then call the function with the associated value. Let’s try it with all the things we have learned so far:

function validate<K extends keyof FormFields>(key: K, forms: FormFields) {
forms[key].validator(forms[key].value) // 💥 TS2345
}

Nope, no can-do! Even though we bound the key to a specific value and we didn’t destructure our arguments, we have no possibility to run this. The problem is that both index accesses are reading operations. Which means that TypeScript just creates a union type for each property:

  • forms[key].validator is of type (val: number) => boolean | (val: string) => boolean
  • forms[key].value is of type number | string

Which means that TypeScript tries to call all possible values of number | string to an intersected function type: (val: number & string) => boolean. number & string is again never, in case you wondered.

And this is something that’s really hard to overcome. Because the moment we do an index access to forms, all we get is are union types. To make this work, we would need forms[key].validator to be (val: number | string ) => boolean. And that requires a bit of a journey.

First of all, let’s create a generic type that represents our fields. This comes in handy later.

type Field<T> = {
value: T,
validator: (val: T) => T
}

type FormFields = {
age: Field<number>,
name: Field<string>
}

With that Field<T> type, we can create a validate function that does what it should do:

function validate_field<T>(obj: Field<T>) {
return obj.validator(obj.value);
}

So far, so good. With that, we can already do validations of the like

validate_field(forms.age);

We still have a little problem once we do an index access:

function validate<K extends keyof FormFields>(key: K, forms: FormFields) {
let obj = forms[key];
validate_field(obj); // 💥 TS2345
}

Same problem. But, since we know better, we can help TypeScript’s type system with a little push into the right direction:

function validate<K extends keyof FormFields>(key: K, forms: FormFields) {
let obj = forms[key];
validate_field(obj as Field<typeof obj.value>);
}

Phew. While we usually don’t want to have type assertions, this one is totally valid. We point TypeScript to a specific branch in our union type and narrow it down to a clear subset. With typeof obj.value and the way Field is structured, there is no ambiguity and we know, that this is correct. The rest is done by the wonderfully type-safe function interfaces!

As an alternative, we could do an explicit type annotation of obj, where I allow a much broader type that encompasses all possible values:

function validate<K extends keyof FormFields>(key: K, forms: FormFields) {
let obj: Field<any> = forms[key];
validate_field(obj);
}

Whatever you like. Do you have more ideas? Please let me know!

Bottom line #

TypeScript has the unique and extraordinary task of attaching a type system to an incredibly flexible language. And TypeScript tries to be as sound as possible in doing so. This means that for some tasks, it gets very strict and rules out cases and statements where there’s no immediately visible problem. And whenever we encounter a situation like that, there are ways to discuss with the type system on what’s correct and what isn’t. That’s the uniqueness, and the power of a gradual type system.

If you want to read more, I highly recommend this issue that details the reasoning for improving the soundness of index access types. There’s also a couple of playgrounds for you

Big shoutout to Ty and Pezi for giving me some brain teasers. This was fun and I hope you gained as many insights as I did!

Related Articles