Everyday TypeScript: Impossible Intersections
Welcome to the Impossible Intersections lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen type intersections like
X & Y, but we left out an important edge case. An intersection means "a type that is simultaneously both of these other types." For example,{name: string} & {email: string}gives us{name: string, email: string}.What happens when we intersect two types that aren't compatible? For example, what does
{x: number} & {x: string}give us? We know thatxcan't hold both a number and a string at the same time.TypeScript's designers could've made this cause a type error, but they didn't. Instead, these impossible intersections are allowed! One reason for this is that preventing impossible intersections would make the compiler much slower.
The code below intersects two incompatible types. Note that we don't actually use the intersected type yet. We'll do that in a moment.
>
type HasEmail1 = {email: string};type HasEmail2 = {email: number};type User = HasEmail1 & HasEmail2;'This code compiles' + ' and runs!';Result:
When we try to use our new
Userintersection type, we get a confusing type error.>
type HasEmail1 = {email: string};type HasEmail2 = {email: number};/* The `email` property's type is `never` because no value can be both a* string and a number. */type User = HasEmail1 & HasEmail2;const user: User = {email: 'amir@example.com'};Result:
type error: Type 'string' is not assignable to type 'never'.
The error above refers to
never, which we saw in an earlier lesson. It's a type that doesn't allow any values at all. Assigning any value to anevercauses a type error like the one above.The error message hints at what our type intersection actually did. No value can be both a
stringand anumberat the same time, so ouremailproperty gets the typenever. The overallUsertype is{email: never}.This
nevererror message may seem vague now, but it's even worse when we have to debug it in a real system. Imagine that type and variable declaration above lives in a different file. OurHasEmail1andHasEmail2types are defined in "email1.ts" and "email2.ts". TheUsertype is defined in "user.ts". And our actualconst user: Uservariable is created in "login.ts".The error message about
nevershows up in "login.ts", where we tried to create the variable. We have to trace that back to the definition ofUserin "user.ts". We then have to open "email1.ts" and "email2.ts" to see howHasEmail1andHasEmail2are defined.We're still not done! Now we have to manually find the conflicting properties on the
HasEmail1andHasEmail2types. In our simple example, each type only has one property, so it's obvious which property caused the problem. But in real-world systems, those types may have dozens of properties, and those properties may themselves be complex nested object types.If we've already defined
HasEmail1andHasEmail2types, then it might seem easy to intersect them with&. But in the long term, we're making more work for ourselves: we risk confusing errors aboutneverin the future.Ideally, we want a type error at the point where we combine the two types to make
User. Specifically, we want to know which properties actually conflict, and we want to know what types those properties have.Fortunately, there's a better approach that accomplishes those goals. We can use
extendsinstead of a type intersection. When an interface extends another interface or type, and the two types have incompatible properties, we get a type error.>
interface HasEmail {email: string}interface User extends HasEmail {name: stringemail: number}const user: User = {name: 'Amir'};user;Result:
type error: Interface 'User' incorrectly extends interface 'HasEmail'. Types of property 'email' are incompatible. Type 'number' is not assignable to type 'string'.Take a moment to read that error message, and compare it to the
nevererror up above. This error is much clearer and more helpful in debugging the code. It tells us the interfaces' names, the property that caused the problem, and even the conflicting properties' individual types.As you might guess, we recommend defaulting to
extendsinstead of type intersections.There's also a broader lesson here. New programmers tend to stop working on code as soon as it works. With more experience, they learn that working code isn't enough. Code must also be understandable and maintainable by other programmers, including programmers who join the team in the future and don't know the code's history.
The same principle applies to static types. Satisfying the type checker is like getting the code to work for the first time. But often, the first version of the code that gets the right answer has problems. It's important to treat static types with the same respect that we treat all other code: they need to work, but they also need to be understandable and maintainable.
In this lesson, we saw that type intersections can cause future maintenance problems that don't exist with interfaces and
extends. When you initially write your code, both methods are easy to use. But as the code evolves, you'll benefit from better error messages withextends.