Skip to content

Commit c5f2ca1

Browse files
committed
Updated type predicates section
1 parent 23fe0aa commit c5f2ca1

File tree

5 files changed

+71
-254
lines changed

5 files changed

+71
-254
lines changed

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

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -973,53 +973,43 @@ Your challenge is to modify the `parseValue` function so that the tests pass and
973973

974974
#### Exercise 3: Reusable Type Guards
975975

976-
Let's imagine that we have two very similar functions, each with a long conditional check to narrow down the type of a value.
976+
Let's imagine that we have two functions which both take in a `value` of type `unknown`, and attempt to parse that value to an array of strings.
977977

978-
Here's the first function:
978+
Here's the first function, which joins an array of names together into a single string:
979979

980980
```typescript
981-
const parseValue = (value: unknown) => {
982-
if (
983-
typeof value === "object" &&
984-
value !== null &&
985-
"data" in value &&
986-
typeof value.data === "object" &&
987-
value.data !== null &&
988-
"id" in value.data &&
989-
typeof value.data.id === "string"
990-
) {
991-
return value.data.id;
981+
const joinNames = (value: unknown) => {
982+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
983+
return value.join(" ");
992984
}
993985

994986
throw new Error("Parsing error!");
995987
};
996988
```
997989

998-
And here's the second function:
990+
And here's the second function, which maps over the array of names and adds a prefix to each one:
999991

1000992
```typescript
1001-
const parseValueAgain = (value: unknown) => {
1002-
if (
1003-
typeof value === "object" &&
1004-
value !== null &&
1005-
"data" in value &&
1006-
typeof value.data === "object" &&
1007-
value.data !== null &&
1008-
"id" in value.data &&
1009-
typeof value.data.id === "string"
1010-
) {
1011-
return value.data.id;
993+
const createSections = (value: unknown) => {
994+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
995+
return value.map((item) => `Section: ${item}`);
1012996
}
1013997

1014998
throw new Error("Parsing error!");
1015999
};
10161000
```
10171001

1018-
Both functions have the same conditional check. This is a great opportunity to create a reusable type guard.
1002+
Both functions have the same conditional check:
1003+
1004+
```ts
1005+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
1006+
```
1007+
1008+
This is a great opportunity to create a reusable type guard.
10191009
10201010
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.
10211011
1022-
<Exercise title="Exercise 3: Reusable Type Guards" filePath="/src/018-unions-and-narrowing/066.5-reusable-type-guards.problem.ts"></Exercise>
1012+
<Exercise title="Exercise 3: Reusable Type Guards" filePath="/src/018-unions-and-narrowing/072.5-reusable-type-guards.problem.ts"></Exercise>
10231013
10241014
#### Solution 1: Narrowing Errors with `instanceof`
10251015
@@ -1168,18 +1158,12 @@ This is usually _not_ how you'd want to write your code. It's a bit of a mess. Y
11681158
11691159
#### Solution 3: Reusable Type Guards
11701160
1171-
The first step is to create a function called `hasDataId` that captures the conditional check:
1161+
The first step is to create a function called `isArrayOfStrings` that captures the conditional check:
11721162
11731163
```typescript
1174-
const hasDataId = (value) => {
1164+
const isArrayOfStrings = (value) => {
11751165
return (
1176-
typeof value === "object" &&
1177-
value !== null &&
1178-
"data" in value &&
1179-
typeof value.data === "object" &&
1180-
value.data !== null &&
1181-
"id" in value.data &&
1182-
typeof value.data.id === "string"
1166+
Array.isArray(value) && value.every((item) => typeof item === "string")
11831167
);
11841168
};
11851169
```
@@ -1189,33 +1173,33 @@ We haven't given `value` a type here - `unknown` makes sense, because it could b
11891173
Now we can refactor the two functions to use this type guard:
11901174
11911175
```typescript
1192-
const parseValue = (value: unknown) => {
1193-
if (hasDataId(value)) {
1194-
return value.data.id;
1176+
const joinNames = (value: unknown) => {
1177+
if (isArrayOfStrings(value)) {
1178+
return value.join(" ");
11951179
}
11961180

11971181
throw new Error("Parsing error!");
11981182
};
11991183

1200-
const parseValueAgain = (value: unknown) => {
1201-
if (hasDataId(value)) {
1202-
return value.data.id;
1184+
const createSections = (value: unknown) => {
1185+
if (isArrayOfStrings(value)) {
1186+
return value.map((item) => `Section: ${item}`);
12031187
}
12041188

12051189
throw new Error("Parsing error!");
12061190
};
12071191
```
12081192
1209-
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.
1193+
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 `isArrayOfStrings` being called on `value` ensures that `value` is an array of strings.
12101194
1211-
We can observe this by hovering over `hasDataId`:
1195+
We can observe this by hovering over `isArrayOfStrings`:
12121196
12131197
```typescript
1214-
// hovering over `hasDataId` shows:
1215-
const hasDataId: (value: unknown) => value is { data: { id: string } };
1198+
// hovering over `isArrayOfStrings` shows:
1199+
const isArrayOfStrings: (value: unknown) => value is string[];
12161200
```
12171201
1218-
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 } }`".
1202+
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 `string[]`".
12191203
12201204
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.
12211205

src/018-unions-and-narrowing/066.5-reusable-type-guards.problem.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

src/018-unions-and-narrowing/066.5-reusable-type-guards.solution.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,48 @@
11
import { Equal, Expect } from "@total-typescript/helpers";
22
import { describe, expect, it } from "vitest";
33

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;
4+
const joinNames = (value: unknown) => {
5+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
6+
return value.join(" ");
157
}
168

179
throw new Error("Parsing error!");
1810
};
1911

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;
12+
const createSections = (value: unknown) => {
13+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
14+
return value.map((item) => `Section: ${item}`);
3115
}
3216

3317
throw new Error("Parsing error!");
3418
};
3519

36-
describe("parseValue", () => {
37-
it("Should handle a { data: { id: string } }", () => {
38-
const result = parseValue({
39-
data: {
40-
id: "123",
41-
},
42-
});
20+
describe("joinNames", () => {
21+
it("Should handle an array of strings", () => {
22+
const result = joinNames(["John", "Doe"]);
4323

4424
type test = Expect<Equal<typeof result, string>>;
4525

46-
expect(result).toBe("123");
26+
expect(result).toBe("John Doe");
4727
});
4828

4929
it("Should error when anything else is passed in", () => {
50-
expect(() => parseValue("123")).toThrow("Parsing error!");
51-
expect(() => parseValue(123)).toThrow("Parsing error!");
30+
expect(() => joinNames("John")).toThrow("Parsing error!");
31+
expect(() => joinNames(123)).toThrow("Parsing error!");
5232
});
5333
});
5434

55-
describe("parseValueAgain", () => {
56-
it("Should handle a { data: { id: string } }", () => {
57-
const result = parseValueAgain({
58-
data: {
59-
id: "123",
60-
},
61-
});
35+
describe("createSections", () => {
36+
it("Should handle an array of strings", () => {
37+
const result = createSections(["John", "Doe"]);
6238

63-
type test = Expect<Equal<typeof result, string>>;
39+
type test = Expect<Equal<typeof result, string[]>>;
6440

65-
expect(result).toBe("123");
41+
expect(result).toEqual(["Section: John", "Section: Doe"]);
6642
});
6743

6844
it("Should error when anything else is passed in", () => {
69-
expect(() => parseValueAgain("123")).toThrow("Parsing error!");
70-
expect(() => parseValueAgain(123)).toThrow("Parsing error!");
45+
expect(() => createSections("John")).toThrow("Parsing error!");
46+
expect(() => createSections(123)).toThrow("Parsing error!");
7147
});
7248
});

0 commit comments

Comments
 (0)