Skip to content

Commit f6d4961

Browse files
committed
Fixed 221 exercise and improved type predicates section
1 parent c5f2ca1 commit f6d4961

File tree

3 files changed

+150
-23
lines changed

3 files changed

+150
-23
lines changed

book-content/chapters/16-the-utils-folder.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,20 @@ function isAlbum(input: unknown) {
424424
}
425425
```
426426

427-
This can feel far too verbose. We can make it more readable by adding our own type predicate.
427+
But at this point, something frustrating happens - TypeScript _stops_ inferring the return value of the function. We can see this by hovering over `isAlbum`:
428+
429+
```typescript
430+
// hovering over isAlbum shows:
431+
function isAlbum(input: unknown): boolean;
432+
```
433+
434+
This is because TypeScript's type predicate inference has limits - it can only process a certain level of complexity.
435+
436+
Not only that, but our code is now _extremely_ defensive. We're checking the existence _and_ type of every property. This is a lot of boilerplate, and might not be necessary. In fact, code like this should probably be encapsulated in a library like [Zod](https://zod.dev/).
437+
438+
### Writing Your Own Type Predicates
439+
440+
To solve this, we can manually annotate our `isAlbum` function with a type predicate:
428441

429442
```typescript
430443
function isAlbum(input: unknown): input is Album {
@@ -439,7 +452,9 @@ function isAlbum(input: unknown): input is Album {
439452
}
440453
```
441454

442-
Now, when we use `isAlbum`, TypeScript will know that the type of the value has been narrowed to `Album`:
455+
This annotation tells TypeScript that when `isAlbum` returns `true`, the type of the value has been narrowed to `Album`.
456+
457+
Now, when we use `isAlbum`, TypeScript will infer it correctly:
443458

444459
```typescript
445460
const run = (maybeAlbum: unknown) => {
@@ -449,19 +464,21 @@ const run = (maybeAlbum: unknown) => {
449464
};
450465
```
451466

452-
For complex type guards, this can be much more readable.
467+
This can ensure that you get the same type behavior from complex type guards.
453468

454469
### Type Predicates Can be Unsafe
455470

456-
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:
471+
Authoring your own type predicates can be a little dangerous. TypeScript doesn't track if the type predicate's runtime behavior matches the type predicate's type signature.
457472

458473
```typescript
459-
function isAlbum(input): input is Album {
460-
return typeof input === "object";
474+
function isNumber(input: unknown): input is number {
475+
return typeof input === "string";
461476
}
462477
```
463478

464-
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 `!`.
479+
In this case, TypeScript _thinks_ that `isNumber` checks if something is a number. But in fact, it checks if something is a string! There are no guarantees that the runtime behavior of the function matches the type signature.
480+
481+
This is a common pitfall when working with type predicates - it's important to consider them about as unsafe as `as` and `!`.
465482

466483
## Assertion Functions
467484

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,68 @@
11
import { Equal, Expect } from "@total-typescript/helpers";
2-
import { expect, it } from "vitest";
2+
import { describe, expect, it } from "vitest";
33

4-
const isString = (input: unknown) => {
5-
return typeof input === "string";
4+
const hasDataAndId = (value: unknown) => {
5+
return (
6+
typeof value === "object" &&
7+
value !== null &&
8+
"data" in value &&
9+
typeof value.data === "object" &&
10+
value.data !== null &&
11+
"id" in value.data &&
12+
typeof value.data.id === "string"
13+
);
614
};
715

8-
it("Should be able to be passed to .filter and work", () => {
9-
const mixedArray = [1, "hello", [], {}];
16+
const parseValue = (value: unknown) => {
17+
if (hasDataAndId(value)) {
18+
return value.data.id;
19+
}
1020

11-
const stringsOnly = mixedArray.filter(isString);
21+
throw new Error("Parsing error!");
22+
};
23+
24+
const parseValueAgain = (value: unknown) => {
25+
if (hasDataAndId(value)) {
26+
return value.data.id;
27+
}
28+
29+
throw new Error("Parsing error!");
30+
};
31+
32+
describe("parseValue", () => {
33+
it("Should handle a { data: { id: string } }", () => {
34+
const result = parseValue({
35+
data: {
36+
id: "123",
37+
},
38+
});
39+
40+
type test = Expect<Equal<typeof result, string>>;
41+
42+
expect(result).toBe("123");
43+
});
44+
45+
it("Should error when anything else is passed in", () => {
46+
expect(() => parseValue("123")).toThrow("Parsing error!");
47+
expect(() => parseValue(123)).toThrow("Parsing error!");
48+
});
49+
});
50+
51+
describe("parseValueAgain", () => {
52+
it("Should handle a { data: { id: string } }", () => {
53+
const result = parseValueAgain({
54+
data: {
55+
id: "123",
56+
},
57+
});
58+
59+
type test = Expect<Equal<typeof result, string>>;
1260

13-
type test1 = Expect<Equal<typeof stringsOnly, string[]>>;
61+
expect(result).toBe("123");
62+
});
1463

15-
expect(stringsOnly).toEqual(["hello"]);
64+
it("Should error when anything else is passed in", () => {
65+
expect(() => parseValueAgain("123")).toThrow("Parsing error!");
66+
expect(() => parseValueAgain(123)).toThrow("Parsing error!");
67+
});
1668
});
Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,74 @@
11
import { Equal, Expect } from "@total-typescript/helpers";
2-
import { expect, it } from "vitest";
2+
import { describe, expect, it } from "vitest";
33

4-
const isString = (input: unknown): input is string => {
5-
return typeof input === "string";
4+
const hasDataAndId = (
5+
value: unknown,
6+
): value is {
7+
data: {
8+
id: string;
9+
};
10+
} => {
11+
return (
12+
typeof value === "object" &&
13+
value !== null &&
14+
"data" in value &&
15+
typeof value.data === "object" &&
16+
value.data !== null &&
17+
"id" in value.data &&
18+
typeof value.data.id === "string"
19+
);
620
};
721

8-
it("Should be able to be passed to .filter and work", () => {
9-
const mixedArray = [1, "hello", [], {}];
22+
const parseValue = (value: unknown) => {
23+
if (hasDataAndId(value)) {
24+
return value.data.id;
25+
}
1026

11-
const stringsOnly = mixedArray.filter(isString);
27+
throw new Error("Parsing error!");
28+
};
29+
30+
const parseValueAgain = (value: unknown) => {
31+
if (hasDataAndId(value)) {
32+
return value.data.id;
33+
}
34+
35+
throw new Error("Parsing error!");
36+
};
37+
38+
describe("parseValue", () => {
39+
it("Should handle a { data: { id: string } }", () => {
40+
const result = parseValue({
41+
data: {
42+
id: "123",
43+
},
44+
});
45+
46+
type test = Expect<Equal<typeof result, string>>;
47+
48+
expect(result).toBe("123");
49+
});
50+
51+
it("Should error when anything else is passed in", () => {
52+
expect(() => parseValue("123")).toThrow("Parsing error!");
53+
expect(() => parseValue(123)).toThrow("Parsing error!");
54+
});
55+
});
56+
57+
describe("parseValueAgain", () => {
58+
it("Should handle a { data: { id: string } }", () => {
59+
const result = parseValueAgain({
60+
data: {
61+
id: "123",
62+
},
63+
});
64+
65+
type test = Expect<Equal<typeof result, string>>;
1266

13-
type test1 = Expect<Equal<typeof stringsOnly, string[]>>;
67+
expect(result).toBe("123");
68+
});
1469

15-
expect(stringsOnly).toEqual(["hello"]);
70+
it("Should error when anything else is passed in", () => {
71+
expect(() => parseValueAgain("123")).toThrow("Parsing error!");
72+
expect(() => parseValueAgain(123)).toThrow("Parsing error!");
73+
});
1674
});

0 commit comments

Comments
 (0)