You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: book-content/chapters/05-unions-literals-and-narrowing.md
+101Lines changed: 101 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -904,6 +904,54 @@ it("Should error when anything else is passed in", () => {
904
904
905
905
Your challenge is to modify the `parseValue` function so that the tests pass and the errors go away. I want you to challenge yourself to do this _only_ by narrowing the type of `value` inside of the function. No changes to the types. This will require a very large `if` statement!
906
906
907
+
#### Exercise 3: Reusable Type Guards
908
+
909
+
Let's imagine that we have two very similar functions, each with a long conditional check to narrow down the type of a value.
910
+
911
+
Here's the first function:
912
+
913
+
```typescript
914
+
const parseValue = (value:unknown) => {
915
+
if (
916
+
typeofvalue==="object"&&
917
+
value!==null&&
918
+
"data"invalue&&
919
+
typeofvalue.data==="object"&&
920
+
value.data!==null&&
921
+
"id"invalue.data&&
922
+
typeofvalue.data.id==="string"
923
+
) {
924
+
returnvalue.data.id;
925
+
}
926
+
927
+
thrownewError("Parsing error!");
928
+
};
929
+
```
930
+
931
+
And here's the second function:
932
+
933
+
```typescript
934
+
const parseValueAgain = (value:unknown) => {
935
+
if (
936
+
typeofvalue==="object"&&
937
+
value!==null&&
938
+
"data"invalue&&
939
+
typeofvalue.data==="object"&&
940
+
value.data!==null&&
941
+
"id"invalue.data&&
942
+
typeofvalue.data.id==="string"
943
+
) {
944
+
returnvalue.data.id;
945
+
}
946
+
947
+
thrownewError("Parsing error!");
948
+
};
949
+
```
950
+
951
+
Both functions have the same conditional check. This is a great opportunity to create a reusable type guard.
952
+
953
+
All the tests are currently passing. Your job is to try to refactor the two functions to use a reusable type guard, and remove the duplicated code. As it turns out, TypeScript makes this a lot easier than you expect.
954
+
907
955
#### Solution 1: Narrowing Errors with `instanceof`
908
956
909
957
The way to solve this challenge is to narrow the `error` using the `instanceof` operator.
@@ -1034,6 +1082,59 @@ Thanks to this huge conditional, our tests pass, and our error messages are gone
1034
1082
1035
1083
This is usually _not_ how you'd want to write your code. It's a bit of a mess. You could use a library like [Zod](https://zod.dev) to do this with a much nicer API. But it's a great way to understand how `unknown` and narrowing work in TypeScript.
1036
1084
1085
+
#### Solution 3: Reusable Type Guards
1086
+
1087
+
The first step is to create a function called `hasDataId` that captures the conditional check:
1088
+
1089
+
```typescript
1090
+
const hasDataId = (value) => {
1091
+
return (
1092
+
typeofvalue==="object"&&
1093
+
value!==null&&
1094
+
"data"invalue&&
1095
+
typeofvalue.data==="object"&&
1096
+
value.data!==null&&
1097
+
"id"invalue.data&&
1098
+
typeofvalue.data.id==="string"
1099
+
);
1100
+
};
1101
+
```
1102
+
1103
+
We haven't given `value` a type here - `unknown` makes sense, because it could be anything.
1104
+
1105
+
Now we can refactor the two functions to use this type guard:
1106
+
1107
+
```typescript
1108
+
const parseValue = (value:unknown) => {
1109
+
if (hasDataId(value)) {
1110
+
returnvalue.data.id;
1111
+
}
1112
+
1113
+
thrownewError("Parsing error!");
1114
+
};
1115
+
1116
+
const parseValueAgain = (value:unknown) => {
1117
+
if (hasDataId(value)) {
1118
+
returnvalue.data.id;
1119
+
}
1120
+
1121
+
thrownewError("Parsing error!");
1122
+
};
1123
+
```
1124
+
1125
+
Incredibly, this is all TypeScript needs to be able to narrow the type of `value` inside of the `if` statement. It's smart enough to understand that `hasDataId` being called on `value` ensures that `value` has a `data` property with an `id` property.
This return type we're seeing is a type predicate. It's a way of saying "if this function returns `true`, then the type of the value is `{ data: { id: string } }`".
1135
+
1136
+
We'll look at authoring our own type predicates in one of the later chapters in the book - but it's very useful that TypeScript infers its own.
1137
+
1037
1138
## Discriminated Unions
1038
1139
1039
1140
In this section we'll look at a common pattern TypeScript developers use to structure their code. It's called a 'discriminated union'.
Copy file name to clipboardExpand all lines: book-content/chapters/16-the-utils-folder.md
+64-28Lines changed: 64 additions & 28 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -326,14 +326,14 @@ Note how clever TypeScript is being here. Even though we didn't specify a return
326
326
327
327
## Type Predicates
328
328
329
-
A type predicate is a special return type that tells TypeScript that a function returns a Boolean value that says something about the type of one of its parameters.
329
+
We were introduced to type predicates way back in chapter 5, when we looked at narrowing. They're used to capture reusable logic that narrows the type of a variable.
330
330
331
331
For example, say we want to ensure that a variable is an `Album` before we try accessing its properties or passing it to a function that requires an `Album`.
332
332
333
-
We can write an `isAlbum` function that takes in an input, and specifies a return type of `input is Album`. The body of the function will check that the `input` passed in is a non-null object with the required properties of an `Album`:
333
+
We can write an `isAlbum` function that takes in an input, and checks for all the required properties.
334
334
335
335
```typescript
336
-
function isAlbum(input:unknown):inputisAlbum {
336
+
function isAlbum(input:unknown) {
337
337
return (
338
338
typeofinput==="object"&&
339
339
input!==null&&
@@ -345,55 +345,91 @@ function isAlbum(input: unknown): input is Album {
345
345
}
346
346
```
347
347
348
-
The `input is Album` return type is the type predicate that tells TypeScript that if the function returns `true`, then the `input` parameter is of type `Album`. Otherwise, it isn't.
348
+
If we hover over `isAlbum`, we can see a rather ugly type signature:
349
349
350
-
### Narrowing with Type Predicates
350
+
```typescript
351
+
// hovering over isAlbum shows:
352
+
function isAlbum(
353
+
input:unknown,
354
+
):inputisobject&
355
+
Record<"id", unknown> &
356
+
Record<"title", unknown> &
357
+
Record<"artist", unknown> &
358
+
Record<"year", unknown>;
359
+
```
351
360
352
-
Type predicates are often used in conditional statements to narrow the type of a variable to become more specific.
361
+
This is technically correct: a big intersection between an `object` and a bunch of `Record`s. But it's not very helpful.
353
362
354
-
For example, we can use the `isAlbum`type predicate to check if an `item` is an `Album` before accessing its `title` property:
363
+
When we try to use `isAlbum`to narrow the type of a value, TypeScript will infer lots of the values as `unknown`:
355
364
356
365
```typescript
357
-
function getAlbumTitle(item:unknown) {
358
-
if (isAlbum(item)) {
359
-
returnitem.title;
366
+
const run = (maybeAlbum:unknown)=> {
367
+
if (isAlbum(maybeAlbum)) {
368
+
maybeAlbum.name.toUpperCase(); // red squiggly line under name
360
369
}
361
-
return"Unknown Album";
362
-
}
370
+
};
371
+
372
+
// hovering over name shows:
373
+
// Object is of type 'unknown'.
363
374
```
364
375
365
-
In this case, the `getAlbumTitle` function takes an `item` of type `unknown`. Inside the function, we use the `isAlbum`type predicate to check if the `item` is an `Album`. If it is, TypeScript narrows the type of `item` to `Album` within the conditional block, allowing us to access the `title` property without any type errors:
376
+
To fix this, we'd need to add even more checks to `isAlbum` to ensure we're checking the types of all the properties:
366
377
367
378
```typescript
368
-
let title =getAlbumTitle({
369
-
id: 1,
370
-
title: "Dummy",
371
-
artist: "Portishead",
372
-
year: 1994,
373
-
});
374
-
375
-
console.log(title); // "Dummy"
379
+
function isAlbum(input:unknown) {
380
+
return (
381
+
typeofinput==="object"&&
382
+
input!==null&&
383
+
"id"ininput&&
384
+
"title"ininput&&
385
+
"artist"ininput&&
386
+
"year"ininput&&
387
+
typeofinput.id==="number"&&
388
+
typeofinput.title==="string"&&
389
+
typeofinput.artist==="string"&&
390
+
typeofinput.year==="number"
391
+
);
392
+
}
393
+
```
376
394
377
-
let notAnAlbumTitle =getAlbumTitle("Some string");
395
+
This can feel far too verbose. We can make it more readable by adding our own type predicate.
378
396
379
-
console.log(notAnAlbumTitle); // "Unknown Album"
397
+
```typescript
398
+
function isAlbum(input:unknown):inputisAlbum {
399
+
return (
400
+
typeofinput==="object"&&
401
+
input!==null&&
402
+
"id"ininput&&
403
+
"title"ininput&&
404
+
"artist"ininput&&
405
+
"year"ininput
406
+
);
407
+
}
380
408
```
381
409
382
-
### Type Predicates Can be Unsafe
410
+
Now, when we use `isAlbum`, TypeScript will know that the type of the value has been narrowed to `Album`:
383
411
384
-
Type predicates are a great technique to be aware of, and are particularly useful when working with union types.
412
+
```typescript
413
+
const run = (maybeAlbum:unknown) => {
414
+
if (isAlbum(maybeAlbum)) {
415
+
maybeAlbum.name.toUpperCase(); // No error!
416
+
}
417
+
};
418
+
```
385
419
386
-
However, there are situations where they aren't as type-safe as they may appear.
420
+
For complex type guards, this can be much more readable.
421
+
422
+
### Type Predicates Can be Unsafe
387
423
388
-
For example, if the type predicate doesn't match the actual type being checked, TypeScript won't catch that discrepancy:
424
+
Authoring your own type predicates can be a little dangerous. If the type predicate doesn't accurately reflect the type being checked, TypeScript won't catch that discrepancy:
389
425
390
426
```typescript
391
427
function isAlbum(input):inputisAlbum {
392
428
returntypeofinput==="object";
393
429
}
394
430
```
395
431
396
-
In this case, any object passed to `isAlbum` will be considered an `Album`, even if it doesn't have the required properties. This is a common pitfall when working with type predicates, and it's important to ensure that the type predicate accurately reflects the type being checked.
432
+
In this case, any object passed to `isAlbum` will be considered an `Album`, even if it doesn't have the required properties. This is a common pitfall when working with type predicates - it's important to consider them about as unsafe as `as` and `!`.
0 commit comments