Everyday TypeScript: Type Widening
Welcome to the Type Widening 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 many examples of type inference in past lessons. For example, if we assign
trueto a variable then its type is inferred asboolean.>
let aBool = true;aBool = false;aBool;Result:
false
TypeScript allowed us to assign
falsetoaBool, so we know that its type must bebooleanrather than the literal typetrue. But why isn't its typetrue? We assignedtrueto the variable, so it seems like its inferred type should betrue.What we're seeing here is a "quality of life" feature in TypeScript. TypeScript helps us out by "widening" the inferred type in some situations, including this one. When we assign
trueorfalseto a variable, TypeScript widens its type toboolean, since that's usually what we want.The same happens for strings: if we assign
'a'to a variable, TypeScript could infer it as the literal type'a'. But in most cases we'll want the type to bestring, so that's what TypeScript infers. Likewise for numbers: if we assign5to a variable, we probably want the inferred type to benumber, so TypeScript does that for us.>
let aString = 'a';aString = 'b';aString;Result:
'b'
>
let aNumber = 5;aNumber = 6;aNumber;Result:
6
Type widening is very convenient... most of the time. Unfortunately, there are some cases where it causes type errors. Here's an example where type widening causes a type error.
(You can write
type errorif an example will result in a type error.)>
type User = {kind: 'user'name: string};function userName(user: User) {return user.name;}const user = {kind: 'user',name: 'Amir',};userName(user);Result:
type error: Argument of type '{ kind: string; name: string; }' is not assignable to parameter of type 'User'. Types of property 'kind' are incompatible. Type 'string' is not assignable to type '"user"'.TypeScript widened our
kindproperty's type from the literal string type'user'to the more general typestring. Unfortunately for us, that makes theuservariable's overall type{kind: string, name: string}. We're trying to pass it as aUserwith a narrower type of{kind: 'user', name: string}. That's a type error.There are two possible solutions here. One is to explicitly declare our variable as
const user: User. That works, but can be inconvenient in more complex situations.The other solution is
as const. When we appendas constto any value, TypeScript won't widen its type. For example, if we assigntrue as constto a variable, its type will betrue. Trying to assignfalselater will cause a type error.>
let justTrue = true as const;justTrue = false;Result:
type error: Type 'false' is not assignable to type 'true'.
We can apply
as constto a single string, a whole object, or any other value. The most commonas constuse is on a single literal string. In the next example, we use it only on the user's name, giving it the literal string type'Amir'. When we try to assign a different name, we'll get a type error.>
let user = {name: 'Amir' as const};user.name = 'Betty';user.name;Result:
type error: Type '"Betty"' is not assignable to type '"Amir"'.
Here's a code problem:
In this code, Amir has
kind: 'user'. However, TypeScript automatically widenskind's type from'user'tostring, causing a type error. Modify theamirobject to prevent the widening, allowing us to pass theamirobject as an argument of typeUser.type User = {kind: 'user'name: string};function userName(user: User) {return user.name;}const amir = {kind: 'user' as const,name: 'Amir',};userName(amir);- Goal:
'Amir'
- Yours:
'Amir'
Type widening and
as constmay seem esoteric at first glance, but they come up surprisingly often in real TypeScript code. They're especially common in code that uses many discriminated union types.