diff --git a/book-content/chapters/05-unions-literals-and-narrowing.md b/book-content/chapters/05-unions-literals-and-narrowing.md
index bc3a883..506c7ee 100644
--- a/book-content/chapters/05-unions-literals-and-narrowing.md
+++ b/book-content/chapters/05-unions-literals-and-narrowing.md
@@ -973,53 +973,43 @@ Your challenge is to modify the `parseValue` function so that the tests pass and
#### Exercise 3: Reusable Type Guards
-Let's imagine that we have two very similar functions, each with a long conditional check to narrow down the type of a value.
+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.
-Here's the first function:
+Here's the first function, which joins an array of names together into a single string:
```typescript
-const parseValue = (value: unknown) => {
- if (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
- ) {
- return value.data.id;
+const joinNames = (value: unknown) => {
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
+ return value.join(" ");
}
throw new Error("Parsing error!");
};
```
-And here's the second function:
+And here's the second function, which maps over the array of names and adds a prefix to each one:
```typescript
-const parseValueAgain = (value: unknown) => {
- if (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
- ) {
- return value.data.id;
+const createSections = (value: unknown) => {
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
+ return value.map((item) => `Section: ${item}`);
}
throw new Error("Parsing error!");
};
```
-Both functions have the same conditional check. This is a great opportunity to create a reusable type guard.
+Both functions have the same conditional check:
+
+```ts
+if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
+```
+
+This is a great opportunity to create a reusable type guard.
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.
-
+
#### Solution 1: Narrowing Errors with `instanceof`
@@ -1168,18 +1158,12 @@ This is usually _not_ how you'd want to write your code. It's a bit of a mess. Y
#### Solution 3: Reusable Type Guards
-The first step is to create a function called `hasDataId` that captures the conditional check:
+The first step is to create a function called `isArrayOfStrings` that captures the conditional check:
```typescript
-const hasDataId = (value) => {
+const isArrayOfStrings = (value) => {
return (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
+ Array.isArray(value) && value.every((item) => typeof item === "string")
);
};
```
@@ -1189,33 +1173,33 @@ We haven't given `value` a type here - `unknown` makes sense, because it could b
Now we can refactor the two functions to use this type guard:
```typescript
-const parseValue = (value: unknown) => {
- if (hasDataId(value)) {
- return value.data.id;
+const joinNames = (value: unknown) => {
+ if (isArrayOfStrings(value)) {
+ return value.join(" ");
}
throw new Error("Parsing error!");
};
-const parseValueAgain = (value: unknown) => {
- if (hasDataId(value)) {
- return value.data.id;
+const createSections = (value: unknown) => {
+ if (isArrayOfStrings(value)) {
+ return value.map((item) => `Section: ${item}`);
}
throw new Error("Parsing error!");
};
```
-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.
+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.
-We can observe this by hovering over `hasDataId`:
+We can observe this by hovering over `isArrayOfStrings`:
```typescript
-// hovering over `hasDataId` shows:
-const hasDataId: (value: unknown) => value is { data: { id: string } };
+// hovering over `isArrayOfStrings` shows:
+const isArrayOfStrings: (value: unknown) => value is string[];
```
-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 } }`".
+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[]`".
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.
diff --git a/src/018-unions-and-narrowing/066.5-reusable-type-guards.problem.ts b/src/018-unions-and-narrowing/066.5-reusable-type-guards.problem.ts
deleted file mode 100644
index 3c27942..0000000
--- a/src/018-unions-and-narrowing/066.5-reusable-type-guards.problem.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { expect, it } from "vitest";
-
-const parseValue = (value: unknown) => {
- if (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
- ) {
- return value.data.id;
- }
-
- throw new Error("Parsing error!");
-};
-
-const parseValueAgain = (value: unknown) => {
- if (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
- ) {
- return value.data.id;
- }
-
- throw new Error("Parsing error!");
-};
-
-it("parseValue should handle a { data: { id: string } }", () => {
- const result = parseValue({
- data: {
- id: "123",
- },
- });
-
- expect(result).toBe("123");
-});
-
-it("parseValue should error when anything else is passed in", () => {
- expect(() => parseValue("123")).toThrow("Parsing error!");
- expect(() => parseValue(123)).toThrow("Parsing error!");
-});
-
-it("parseValueAgain should handle a { data: { id: string } }", () => {
- const result = parseValueAgain({
- data: {
- id: "123",
- },
- });
-
- expect(result).toBe("123");
-});
-
-it("parseValueAgain should error when anything else is passed in", () => {
- expect(() => parseValueAgain("123")).toThrow("Parsing error!");
- expect(() => parseValueAgain(123)).toThrow("Parsing error!");
-});
diff --git a/src/018-unions-and-narrowing/066.5-reusable-type-guards.solution.ts b/src/018-unions-and-narrowing/066.5-reusable-type-guards.solution.ts
deleted file mode 100644
index 15690de..0000000
--- a/src/018-unions-and-narrowing/066.5-reusable-type-guards.solution.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { expect, it } from "vitest";
-
-// TODO - remove return type in 5.5
-const hasDataId = (value: unknown): value is { data: { id: string } } => {
- return (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
- );
-};
-
-const parseValue = (value: unknown) => {
- if (hasDataId(value)) {
- return value.data.id;
- }
-
- throw new Error("Parsing error!");
-};
-
-const parseValueAgain = (value: unknown) => {
- if (hasDataId(value)) {
- return value.data.id;
- }
-
- throw new Error("Parsing error!");
-};
-
-it("parseValue should handle a { data: { id: string } }", () => {
- const result = parseValue({
- data: {
- id: "123",
- },
- });
-
- expect(result).toBe("123");
-});
-
-it("parseValue should error when anything else is passed in", () => {
- expect(() => parseValue("123")).toThrow("Parsing error!");
- expect(() => parseValue(123)).toThrow("Parsing error!");
-});
-
-it("parseValueAgain should handle a { data: { id: string } }", () => {
- const result = parseValueAgain({
- data: {
- id: "123",
- },
- });
-
- expect(result).toBe("123");
-});
-
-it("parseValueAgain should error when anything else is passed in", () => {
- expect(() => parseValueAgain("123")).toThrow("Parsing error!");
- expect(() => parseValueAgain(123)).toThrow("Parsing error!");
-});
diff --git a/src/018-unions-and-narrowing/072.5-reusable-type-guards.problem.ts b/src/018-unions-and-narrowing/072.5-reusable-type-guards.problem.ts
index 7df4d63..6201b5b 100644
--- a/src/018-unions-and-narrowing/072.5-reusable-type-guards.problem.ts
+++ b/src/018-unions-and-narrowing/072.5-reusable-type-guards.problem.ts
@@ -1,72 +1,48 @@
import { Equal, Expect } from "@total-typescript/helpers";
import { describe, expect, it } from "vitest";
-const parseValue = (value: unknown) => {
- if (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
- ) {
- return value.data.id;
+const joinNames = (value: unknown) => {
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
+ return value.join(" ");
}
throw new Error("Parsing error!");
};
-const parseValueAgain = (value: unknown) => {
- if (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
- ) {
- return value.data.id;
+const createSections = (value: unknown) => {
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
+ return value.map((item) => `Section: ${item}`);
}
throw new Error("Parsing error!");
};
-describe("parseValue", () => {
- it("Should handle a { data: { id: string } }", () => {
- const result = parseValue({
- data: {
- id: "123",
- },
- });
+describe("joinNames", () => {
+ it("Should handle an array of strings", () => {
+ const result = joinNames(["John", "Doe"]);
type test = Expect>;
- expect(result).toBe("123");
+ expect(result).toBe("John Doe");
});
it("Should error when anything else is passed in", () => {
- expect(() => parseValue("123")).toThrow("Parsing error!");
- expect(() => parseValue(123)).toThrow("Parsing error!");
+ expect(() => joinNames("John")).toThrow("Parsing error!");
+ expect(() => joinNames(123)).toThrow("Parsing error!");
});
});
-describe("parseValueAgain", () => {
- it("Should handle a { data: { id: string } }", () => {
- const result = parseValueAgain({
- data: {
- id: "123",
- },
- });
+describe("createSections", () => {
+ it("Should handle an array of strings", () => {
+ const result = createSections(["John", "Doe"]);
- type test = Expect>;
+ type test = Expect>;
- expect(result).toBe("123");
+ expect(result).toEqual(["Section: John", "Section: Doe"]);
});
it("Should error when anything else is passed in", () => {
- expect(() => parseValueAgain("123")).toThrow("Parsing error!");
- expect(() => parseValueAgain(123)).toThrow("Parsing error!");
+ expect(() => createSections("John")).toThrow("Parsing error!");
+ expect(() => createSections(123)).toThrow("Parsing error!");
});
});
diff --git a/src/018-unions-and-narrowing/072.5-reusable-type-guards.solution.ts b/src/018-unions-and-narrowing/072.5-reusable-type-guards.solution.ts
index 5719d9b..0486dd0 100644
--- a/src/018-unions-and-narrowing/072.5-reusable-type-guards.solution.ts
+++ b/src/018-unions-and-narrowing/072.5-reusable-type-guards.solution.ts
@@ -1,74 +1,54 @@
import { Equal, Expect } from "@total-typescript/helpers";
import { describe, expect, it } from "vitest";
-const hasDataAndId = (
- value: unknown,
-): value is {
- data: {
- id: string;
- };
-} => {
+const isArrayOfStrings = (value: unknown) => {
return (
- typeof value === "object" &&
- value !== null &&
- "data" in value &&
- typeof value.data === "object" &&
- value.data !== null &&
- "id" in value.data &&
- typeof value.data.id === "string"
+ Array.isArray(value) && value.every((item) => typeof item === "string")
);
};
-const parseValue = (value: unknown) => {
- if (hasDataAndId(value)) {
- return value.data.id;
+const joinNames = (value: unknown) => {
+ if (isArrayOfStrings(value)) {
+ return value.join(" ");
}
throw new Error("Parsing error!");
};
-const parseValueAgain = (value: unknown) => {
- if (hasDataAndId(value)) {
- return value.data.id;
+const createSections = (value: unknown) => {
+ if (isArrayOfStrings(value)) {
+ return value.map((item) => `Section: ${item}`);
}
throw new Error("Parsing error!");
};
-describe("parseValue", () => {
- it("Should handle a { data: { id: string } }", () => {
- const result = parseValue({
- data: {
- id: "123",
- },
- });
+describe("joinNames", () => {
+ it("Should handle an array of strings", () => {
+ const result = joinNames(["John", "Doe"]);
type test = Expect>;
- expect(result).toBe("123");
+ expect(result).toBe("John Doe");
});
it("Should error when anything else is passed in", () => {
- expect(() => parseValue("123")).toThrow("Parsing error!");
- expect(() => parseValue(123)).toThrow("Parsing error!");
+ expect(() => joinNames("John")).toThrow("Parsing error!");
+ expect(() => joinNames(123)).toThrow("Parsing error!");
});
});
-describe("parseValueAgain", () => {
- it("Should handle a { data: { id: string } }", () => {
- const result = parseValueAgain({
- data: {
- id: "123",
- },
- });
+describe("createSections", () => {
+ it("Should handle an array of strings", () => {
+ const result = createSections(["John", "Doe"]);
- type test = Expect>;
+ type test = Expect>;
- expect(result).toBe("123");
+ expect(result).toEqual(["Section: John", "Section: Doe"]);
});
it("Should error when anything else is passed in", () => {
- expect(() => parseValueAgain("123")).toThrow("Parsing error!");
- expect(() => parseValueAgain(123)).toThrow("Parsing error!");
+ expect(() => createSections("John")).toThrow("Parsing error!");
+ expect(() => createSections(123)).toThrow("Parsing error!");
});
});