Skip to content

Commit

Permalink
Fixed 221 exercise and improved type predicates section
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpocock committed Jul 3, 2024
1 parent c5f2ca1 commit f6d4961
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 23 deletions.
31 changes: 24 additions & 7 deletions book-content/chapters/16-the-utils-folder.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,20 @@ function isAlbum(input: unknown) {
}
```

This can feel far too verbose. We can make it more readable by adding our own type predicate.
But at this point, something frustrating happens - TypeScript _stops_ inferring the return value of the function. We can see this by hovering over `isAlbum`:

```typescript
// hovering over isAlbum shows:
function isAlbum(input: unknown): boolean;
```

This is because TypeScript's type predicate inference has limits - it can only process a certain level of complexity.

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/).

### Writing Your Own Type Predicates

To solve this, we can manually annotate our `isAlbum` function with a type predicate:

```typescript
function isAlbum(input: unknown): input is Album {
Expand All @@ -439,7 +452,9 @@ function isAlbum(input: unknown): input is Album {
}
```

Now, when we use `isAlbum`, TypeScript will know that the type of the value has been narrowed to `Album`:
This annotation tells TypeScript that when `isAlbum` returns `true`, the type of the value has been narrowed to `Album`.

Now, when we use `isAlbum`, TypeScript will infer it correctly:

```typescript
const run = (maybeAlbum: unknown) => {
Expand All @@ -449,19 +464,21 @@ const run = (maybeAlbum: unknown) => {
};
```

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

### Type Predicates Can be Unsafe

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:
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.

```typescript
function isAlbum(input): input is Album {
return typeof input === "object";
function isNumber(input: unknown): input is number {
return typeof input === "string";
}
```

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 `!`.
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.

This is a common pitfall when working with type predicates - it's important to consider them about as unsafe as `as` and `!`.

## Assertion Functions

Expand Down
68 changes: 60 additions & 8 deletions src/085-the-utils-folder/221-type-predicates.problem.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,68 @@
import { Equal, Expect } from "@total-typescript/helpers";
import { expect, it } from "vitest";
import { describe, expect, it } from "vitest";

const isString = (input: unknown) => {
return typeof input === "string";
const hasDataAndId = (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"
);
};

it("Should be able to be passed to .filter and work", () => {
const mixedArray = [1, "hello", [], {}];
const parseValue = (value: unknown) => {
if (hasDataAndId(value)) {
return value.data.id;
}

const stringsOnly = mixedArray.filter(isString);
throw new Error("Parsing error!");
};

const parseValueAgain = (value: unknown) => {
if (hasDataAndId(value)) {
return value.data.id;
}

throw new Error("Parsing error!");
};

describe("parseValue", () => {
it("Should handle a { data: { id: string } }", () => {
const result = parseValue({
data: {
id: "123",
},
});

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

expect(result).toBe("123");
});

it("Should error when anything else is passed in", () => {
expect(() => parseValue("123")).toThrow("Parsing error!");
expect(() => parseValue(123)).toThrow("Parsing error!");
});
});

describe("parseValueAgain", () => {
it("Should handle a { data: { id: string } }", () => {
const result = parseValueAgain({
data: {
id: "123",
},
});

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

type test1 = Expect<Equal<typeof stringsOnly, string[]>>;
expect(result).toBe("123");
});

expect(stringsOnly).toEqual(["hello"]);
it("Should error when anything else is passed in", () => {
expect(() => parseValueAgain("123")).toThrow("Parsing error!");
expect(() => parseValueAgain(123)).toThrow("Parsing error!");
});
});
74 changes: 66 additions & 8 deletions src/085-the-utils-folder/221-type-predicates.solution.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,74 @@
import { Equal, Expect } from "@total-typescript/helpers";
import { expect, it } from "vitest";
import { describe, expect, it } from "vitest";

const isString = (input: unknown): input is string => {
return typeof input === "string";
const hasDataAndId = (
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"
);
};

it("Should be able to be passed to .filter and work", () => {
const mixedArray = [1, "hello", [], {}];
const parseValue = (value: unknown) => {
if (hasDataAndId(value)) {
return value.data.id;
}

const stringsOnly = mixedArray.filter(isString);
throw new Error("Parsing error!");
};

const parseValueAgain = (value: unknown) => {
if (hasDataAndId(value)) {
return value.data.id;
}

throw new Error("Parsing error!");
};

describe("parseValue", () => {
it("Should handle a { data: { id: string } }", () => {
const result = parseValue({
data: {
id: "123",
},
});

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

expect(result).toBe("123");
});

it("Should error when anything else is passed in", () => {
expect(() => parseValue("123")).toThrow("Parsing error!");
expect(() => parseValue(123)).toThrow("Parsing error!");
});
});

describe("parseValueAgain", () => {
it("Should handle a { data: { id: string } }", () => {
const result = parseValueAgain({
data: {
id: "123",
},
});

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

type test1 = Expect<Equal<typeof stringsOnly, string[]>>;
expect(result).toBe("123");
});

expect(stringsOnly).toEqual(["hello"]);
it("Should error when anything else is passed in", () => {
expect(() => parseValueAgain("123")).toThrow("Parsing error!");
expect(() => parseValueAgain(123)).toThrow("Parsing error!");
});
});

0 comments on commit f6d4961

Please sign in to comment.