Skip to content

Commit 23dde8b

Browse files
committed
Added section on automatically inferred type predicates for 5.5
1 parent 00674f1 commit 23dde8b

File tree

4 files changed

+268
-1
lines changed

4 files changed

+268
-1
lines changed

book-content/chapters/05-unions-literals-and-narrowing.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,54 @@ it("Should error when anything else is passed in", () => {
904904

905905
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!
906906

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+
typeof value === "object" &&
917+
value !== null &&
918+
"data" in value &&
919+
typeof value.data === "object" &&
920+
value.data !== null &&
921+
"id" in value.data &&
922+
typeof value.data.id === "string"
923+
) {
924+
return value.data.id;
925+
}
926+
927+
throw new Error("Parsing error!");
928+
};
929+
```
930+
931+
And here's the second function:
932+
933+
```typescript
934+
const parseValueAgain = (value: unknown) => {
935+
if (
936+
typeof value === "object" &&
937+
value !== null &&
938+
"data" in value &&
939+
typeof value.data === "object" &&
940+
value.data !== null &&
941+
"id" in value.data &&
942+
typeof value.data.id === "string"
943+
) {
944+
return value.data.id;
945+
}
946+
947+
throw new Error("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+
907955
#### Solution 1: Narrowing Errors with `instanceof`
908956

909957
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
10341082

10351083
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.
10361084

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+
typeof value === "object" &&
1093+
value !== null &&
1094+
"data" in value &&
1095+
typeof value.data === "object" &&
1096+
value.data !== null &&
1097+
"id" in value.data &&
1098+
typeof value.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+
return value.data.id;
1111+
}
1112+
1113+
throw new Error("Parsing error!");
1114+
};
1115+
1116+
const parseValueAgain = (value: unknown) => {
1117+
if (hasDataId(value)) {
1118+
return value.data.id;
1119+
}
1120+
1121+
throw new Error("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.
1126+
1127+
We can observe this by hovering over `hasDataId`:
1128+
1129+
```typescript
1130+
// hovering over `hasDataId` shows:
1131+
const hasDataId: (value: unknown) => value is { data: { id: string } };
1132+
```
1133+
1134+
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+
10371138
## Discriminated Unions
10381139

10391140
In this section we'll look at a common pattern TypeScript developers use to structure their code. It's called a 'discriminated union'.

package.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,27 @@
568568
"e-181.2": "tt-cli run 181.2",
569569
"s-181.2": "tt-cli run 181.2 --solution",
570570
"e-181.4": "tt-cli run 181.4",
571-
"s-181.4": "tt-cli run 181.4 --solution"
571+
"s-181.4": "tt-cli run 181.4 --solution",
572+
"e-104.5": "tt-cli run 104.5",
573+
"s-104.5": "tt-cli run 104.5 --solution",
574+
"e-082.5": "tt-cli run 082.5",
575+
"s-082.5": "tt-cli run 082.5 --solution",
576+
"e-086.5": "tt-cli run 086.5",
577+
"s-086.5": "tt-cli run 086.5 --solution",
578+
"e-080.5": "tt-cli run 080.5",
579+
"s-080.5": "tt-cli run 080.5 --solution",
580+
"e-154.8": "tt-cli run 154.8",
581+
"s-154.8": "tt-cli run 154.8 --solution",
582+
"e-154.9": "tt-cli run 154.9",
583+
"s-154.9": "tt-cli run 154.9 --solution",
584+
"e-196.5": "tt-cli run 196.5",
585+
"s-196.5": "tt-cli run 196.5 --solution",
586+
"e-202.5": "tt-cli run 202.5",
587+
"s-202.5": "tt-cli run 202.5 --solution",
588+
"e-202.6": "tt-cli run 202.6",
589+
"s-202.6": "tt-cli run 202.6 --solution",
590+
"e-202.7": "tt-cli run 202.7",
591+
"s-202.7": "tt-cli run 202.7 --solution"
572592
},
573593
"dependencies": {
574594
"@tanstack/react-query": "^4.29.12",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Equal, Expect } from "@total-typescript/helpers";
2+
import { describe, expect, it } from "vitest";
3+
4+
const parseValue = (value: unknown) => {
5+
if (
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+
) {
14+
return value.data.id;
15+
}
16+
17+
throw new Error("Parsing error!");
18+
};
19+
20+
const parseValueAgain = (value: unknown) => {
21+
if (
22+
typeof value === "object" &&
23+
value !== null &&
24+
"data" in value &&
25+
typeof value.data === "object" &&
26+
value.data !== null &&
27+
"id" in value.data &&
28+
typeof value.data.id === "string"
29+
) {
30+
return value.data.id;
31+
}
32+
33+
throw new Error("Parsing error!");
34+
};
35+
36+
describe("parseValue", () => {
37+
it("Should handle a { data: { id: string } }", () => {
38+
const result = parseValue({
39+
data: {
40+
id: "123",
41+
},
42+
});
43+
44+
type test = Expect<Equal<typeof result, string>>;
45+
46+
expect(result).toBe("123");
47+
});
48+
49+
it("Should error when anything else is passed in", () => {
50+
expect(() => parseValue("123")).toThrow("Parsing error!");
51+
expect(() => parseValue(123)).toThrow("Parsing error!");
52+
});
53+
});
54+
55+
describe("parseValueAgain", () => {
56+
it("Should handle a { data: { id: string } }", () => {
57+
const result = parseValueAgain({
58+
data: {
59+
id: "123",
60+
},
61+
});
62+
63+
type test = Expect<Equal<typeof result, string>>;
64+
65+
expect(result).toBe("123");
66+
});
67+
68+
it("Should error when anything else is passed in", () => {
69+
expect(() => parseValueAgain("123")).toThrow("Parsing error!");
70+
expect(() => parseValueAgain(123)).toThrow("Parsing error!");
71+
});
72+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Equal, Expect } from "@total-typescript/helpers";
2+
import { describe, expect, it } from "vitest";
3+
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+
);
20+
};
21+
22+
const parseValue = (value: unknown) => {
23+
if (hasDataAndId(value)) {
24+
return value.data.id;
25+
}
26+
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>>;
66+
67+
expect(result).toBe("123");
68+
});
69+
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+
});
74+
});

0 commit comments

Comments
 (0)