Skip to content

Commit 9c747f0

Browse files
committed
Finished 16
1 parent 4f207f0 commit 9c747f0

File tree

1 file changed

+55
-112
lines changed

1 file changed

+55
-112
lines changed

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

Lines changed: 55 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,30 @@ Here, TypeScript is telling us that `42` is not a `string`. This is because we'v
146146

147147
Passing type arguments is an instruction to TypeScript override inference. If you pass in a type argument, TypeScript will use it as the source of truth. If you don't, TypeScript will use the type of the runtime argument as the source of truth.
148148

149+
### There Is No Such Thing As 'A Generic'
150+
151+
A quick note on terminology here. TypeScript 'generics' has a reputation for being difficult to understand. I think a large part of that is based on how people use the word 'generic'.
152+
153+
A lot of people think of a 'generic' as a part of TypeScript. They think of it like a noun. If you ask someone "where's the 'generic' in this piece of code?":
154+
155+
```typescript
156+
const identity = <T>(arg: T) => arg;
157+
```
158+
159+
They will probably point to the `<T>`. Others might describe the code below as "passing a 'generic' to `Set`":
160+
161+
```typescript
162+
const set = new Set<number>([1, 2, 3]);
163+
```
164+
165+
This terminology gets very confusing. Instead, I prefer to split them into different terms:
166+
167+
- Type Parameter: The `<T>` in `identity<T>`.
168+
- Type Argument: The `number` passed to `Set<number>`.
169+
- Generic Class/Function/Type: A class, function or type that declares a type parameter.
170+
171+
When you break generics down into these terms, it becomes much easier to understand.
172+
149173
### The Problem Generic Functions Solve
150174

151175
Let's put what we've learned into practice.
@@ -268,7 +292,7 @@ Our `TObj` type parameter, when used without a constraint, is treated as `unknow
268292
To fix this, we can add a constraint to `TObj` that ensures it has an `id` property:
269293

270294
```typescript
271-
const removeId = <TObj extends { id: any }>(obj: TObj) => {
295+
const removeId = <TObj extends { id: unknown }>(obj: TObj) => {
272296
const { id, ...rest } = obj;
273297
return rest;
274298
};
@@ -280,7 +304,7 @@ Now, when we use `removeId`, TypeScript will error if we don't pass in an object
280304
const result = removeId({ name: "Alice" }); // red squiggly line under name
281305

282306
// hovering over name shows:
283-
// Object literal may only specify known properties, and 'name' does not exist in type '{ id: any; }'
307+
// Object literal may only specify known properties, and 'name' does not exist in type '{ id: unknown; }'
284308
```
285309

286310
But if we pass in an object with an `id` property, TypeScript will know that `id` has been removed:
@@ -526,11 +550,9 @@ This error shows us that we're trying to call `searchMusic` with two arguments,
526550

527551
### Function Overloads vs Unions
528552

529-
<!-- CONTINUE -->
530-
531-
Function overloads can be useful when you want to express multiple ways to call a function that spread out over different parameters. In the example above, we can either call the function with separate arguments or with a single object.
553+
Function overloads can be useful when you have multiple call signatures spread over different sets of arguments. In the example above, we can either call the function with one argument, or three.
532554

533-
When you have the same number of arguments but different types, you can use a union type instead of function overloads. For example, if you want to allow the user to search by either artist name or criteria object, you could use a union type:
555+
When you have the same number of arguments but different types, you should use a union type instead of function overloads. For example, if you want to allow the user to search by either artist name or criteria object, you could use a union type:
534556

535557
```typescript
536558
function searchMusic(
@@ -540,12 +562,14 @@ function searchMusic(
540562
// Search by artist
541563
searchByArtist(query);
542564
} else {
543-
// Search by genre
544-
searchByGenre(query.genre);
565+
// Search by all
566+
search(query.artist, query.genre, query.year);
545567
}
546568
}
547569
```
548570

571+
This uses far fewer lines of code than defining two overloads and an implementation.
572+
549573
## Exercises
550574

551575
### Exercise 1: Make a Function Generic
@@ -558,9 +582,7 @@ const createStringMap = () => {
558582
};
559583
```
560584

561-
As it currently is, the keys and values for the Map can be of any type.
562-
563-
However, the goal is to make this function generic so that we can pass in a type argument to define the type of the values in the `Map`.
585+
As it currently stands, we get back a `Map<any, any>`. However, the goal is to make this function generic so that we can pass in a type argument to define the type of the values in the `Map`.
564586

565587
For example, if we pass in `number` as the type argument, the function should return a `Map` with values of type `number`:
566588

@@ -597,7 +619,7 @@ const unknownMap = createStringMap();
597619
type test = Expect<Equal<typeof unknownMap, Map<string, unknown>>>; // red squiggly line under Equal<>
598620
```
599621

600-
Your task is to transform `createStringMap` into a generic function capable of accepting a type argument for the values of Map. Make sure it functions as expected for the provided test cases.
622+
Your task is to transform `createStringMap` into a generic function capable of accepting a type argument to describe the values of Map. Make sure it functions as expected for the provided test cases.
601623

602624
### Exercise 2: Default Type Arguments
603625

@@ -624,7 +646,7 @@ const uniqueArray = (arr: any[]) => {
624646

625647
The function accepts an array as an argument, then converts it to a `Set`, then returns it as a new array. This is a common pattern for when you want to have unique values inside your array.
626648

627-
While this function operates effectively at runtime, it lacks type safety. It currently allows an array of `any` type, and as seen in the tests, the return type is also typed as `any`:
649+
While this function operates effectively at runtime, it lacks type safety. It transforms any array passed in into `any[]`.
628650

629651
```typescript
630652
it("returns an array of unique values", () => {
@@ -644,9 +666,9 @@ it("should work on strings", () => {
644666
});
645667
```
646668

647-
Your task is to boost the type safety of the `uniqueArray` function. To accomplish this, you'll need to incorporate a type parameter into the function.
669+
Your task is to boost the type safety of the `uniqueArray` function by making it generic.
648670

649-
Note that in the tests, we do not explicitly provide type arguments when invoking the function. TypeScript should be able to deduce the type from the argument.
671+
Note that in the tests, we do not explicitly provide type arguments when invoking the function. TypeScript should be able to infer the type from the argument.
650672

651673
Adjust the function and insert the necessary type annotations to ensure that the `result` type in both tests is inferred as `number[]` and `string[]`, respectively.
652674

@@ -665,7 +687,7 @@ const addCodeToError = <TError>(error: TError) => {
665687
};
666688

667689
// hovering over code shows
668-
Property 'code' does not exist on type 'TError'.
690+
// Property 'code' does not exist on type 'TError'.
669691
```
670692

671693
If the incoming error doesn't include a `code`, the function assigns a default `UNKNOWN_CODE`. Currently there is an error under the `code` property.
@@ -733,9 +755,7 @@ In short, the thing that we get back from `safeFunction` should either be the th
733755

734756
However, there are some issues with the current type definitions.
735757

736-
The `PromiseFunc` type is currently set to always return `Promise<any>`, which doesn't provide much information.
737-
738-
Also, the function returned by `safeFunction` is supposed to return either the result of `func` or an `Error`, but at the moment, it's just returning `Promise<any>`.
758+
The `PromiseFunc` type is currently set to always return `Promise<any>`. This means that the function returned by `safeFunction` is supposed to return either the result of `func` or an `Error`, but at the moment, it's just returning `Promise<any>`.
739759

740760
There are several tests that are failing due to these issues:
741761

@@ -791,9 +811,9 @@ async (...args: any[]) => {
791811
};
792812
```
793813

794-
Since the thing being passed into `safeFunction` can receive arguments, the function we get back should also contain those arguments and require you to pass them in.
814+
Now that the function being passed into `safeFunction` can receive arguments, the function we get back should _also_ contain those arguments and require you to pass them in.
795815

796-
However, as seen in the tests, the type is currently a bit too wide:
816+
However, as seen in the tests, this isn't working:
797817

798818
```typescript
799819
it("should return the result if the function succeeds", async () => {
@@ -852,41 +872,6 @@ it("should return the result if the function succeeds", async () => {
852872
853873
Update the types of the function and the generic type, and make these tests pass successfully.
854874
855-
### Exercise 7: Type Predicates
856-
857-
Here we have an `isString` function that accepts an input of `unknown` type, and returns a boolean based on whether the `input` is of type string or not:
858-
859-
```typescript
860-
const isString = (input: unknown) => {
861-
return typeof input === "string";
862-
};
863-
```
864-
865-
In this case, the `unknown` type is appropriate as we don't possess any prior knowledge about the type of `input`.
866-
867-
The function is then applied in the context of a `filter` function:
868-
869-
```typescript
870-
const mixedArray = [1, "hello", [], {}];
871-
872-
const stringsOnly = mixedArray.filter(isString);
873-
```
874-
875-
We would anticipate that the `filter` function would return an array of strings since it only retains the elements from `mixedArray` that pass the `isString` test.
876-
877-
However, this doesn't work as expected on the type level.
878-
879-
We end up with an array of empty objects instead, because an empty object is being passed to the `isString` function, and all the other types are assignable to an empty object.
880-
881-
```typescript
882-
// hovering over stringsOnly shows:
883-
const stringsOnly: {}[];
884-
```
885-
886-
In order to make `isString` function as we expect, we need to use a type guard function and add a type predicate to it.
887-
888-
Your task is to adjust the `isString` function to incorporate a type guard and type predicate that will ensure the `filter` function correctly identifies strings as well as assigning the accurate type to the output array.
889-
890875
### Exercise 8: Assertion Functions
891876
892877
This exercise starts with an interface `User`, which has properties `id` and `name`. Then we have an interface `AdminUser`, which extends `User`, inheriting all its properties and adding a `roles` string array property:
@@ -951,7 +936,7 @@ const createStringMap = <T>() => {
951936
952937
With this change, our `createStringMap` function can now handle a type argument `T`.
953938
954-
The error has disappeared from the `numberMap` variable, but the function is still returning a `Map` of type `any, any`:
939+
The error has disappeared from the `numberMap` variable, but the function is still returning a `Map<any, any>`:
955940
956941
```typescript
957942
const numberMap = createStringMap<number>();
@@ -981,7 +966,7 @@ const objMap = createStringMap();
981966
const objMap: Map<string, unknown>;
982967
```
983968
984-
Through these steps, we've successfully transformed `createStringMap` from a regular function into a generic function capable of handling type arguments.
969+
Through these steps, we've successfully transformed `createStringMap` from a regular function into a generic function capable of receiving type arguments .
985970
986971
### Solution 2: Default Type Arguments
987972
@@ -1001,7 +986,7 @@ Now when we call `createStringMap()` without a type argument, we end up with a `
1001986
const stringMap = createStringMap();
1002987

1003988
// hovering over stringMap shows:
1004-
const stringMap: <string>() => Map<string, string>;
989+
const stringMap: Map<string, string>;
1005990
```
1006991
1007992
If we attempt to assign a number as a value, TypeScript gives us an error because it expects a string:
@@ -1040,7 +1025,7 @@ const uniqueArray = <T>(arr: any[]) => {
10401025
};
10411026
```
10421027
1043-
Now when we hover over a call to `uniqueArray`, we can see that it is typed as unknown:
1028+
Now when we hover over a call to `uniqueArray`, we can see that it is inferring the type as `unknown`:
10441029
10451030
```typescript
10461031
const result = uniqueArray([1, 1, 2, 3, 4, 4, 5]);
@@ -1060,7 +1045,7 @@ const uniqueArray = <T>(arr: any[]): T[] => {
10601045
...
10611046
```
10621047
1063-
Now the call to `uniqueArray` is inferred as returning an `unknown` array:
1048+
Now the result of `uniqueArray` is inferred as an `unknown` array:
10641049
10651050
```typescript
10661051
const result = uniqueArray([1, 1, 2, 3, 4, 4, 5]);
@@ -1091,9 +1076,9 @@ const uniqueArray = <T>(arr: T[]): T[] => {
10911076
...
10921077
```
10931078
1094-
The function's return type is an array of `T`, where `T` represents the type of elements supplied to the function.
1079+
The function's return type is an array of `T`, where `T` represents the type of elements in the array supplied to the function.
10951080
1096-
Thus, TypeScript can infer the return type as `number[]` for an input array of numbers, or `string[]` for an input array of strings, even without explicit return type annotation. As we can see, the tests pass successfully:
1081+
Thus, TypeScript can infer the return type as `number[]` for an input array of numbers, or `string[]` for an input array of strings, even without explicit return type annotations. As we can see, the tests pass successfully:
10971082
10981083
```typescript
10991084
// number test
@@ -1107,27 +1092,7 @@ const result = uniqueArray(["a", "b", "b", "c", "c", "c"]);
11071092
type test = Expect<Equal<typeof result, string[]>>;
11081093
```
11091094
1110-
If you explicitly pass a type argument, TypeScript will use it. If you don't, TypeScript attempts to deduce it from the runtime arguments.
1111-
1112-
For example, if you try to pass a boolean in with the number array when providing a type argument, TypeScript will throw an error:
1113-
1114-
```typescript
1115-
const result = uniqueArray<number>([1, 1, 2, 3, 4, 4, 5, true]); // red squiggly line under true
1116-
1117-
// hovering over true shows:
1118-
Type 'boolean' is not assignable to type 'number'.
1119-
```
1120-
1121-
However, without the type argument there will be no error and the type will be inferred as an array of `number | boolean`:
1122-
1123-
```typescript
1124-
const result = uniqueArray([1, 1, 2, 3, 4, 4, 5, true]);
1125-
1126-
// hovering over uniqueArray shows:
1127-
const uniqueArray: <number | boolean>(arr: (number | boolean)[]) => (number | boolean)[]
1128-
```
1129-
1130-
The flexibility of generics and inference allows for dynamic code while still ensuring type safety.
1095+
If you explicitly pass a type argument, TypeScript will use it. If you don't, TypeScript attempts to infer it from the runtime arguments.
11311096
11321097
### Solution 4: Type Parameter Constraints
11331098
@@ -1150,7 +1115,7 @@ const addCodeToError = <TError extends { message: string; code?: number }>(
11501115
11511116
This change ensures that `addCodeToError` must be called with an object that includes a `message` string property. TypeScript also knows that `code` could either be a number or `undefined`. If `code` is absent, it will default to `UNKNOWN_CODE`.
11521117
1153-
These constraints have our tests passing, including the case where we pass in an extra `filepath` property. This is because using `extends` in generics does not restrict you to only passing in the properties defined in the constraint.
1118+
These constraints make our tests pass, including the case where we pass in an extra `filepath` property. This is because using `extends` in generics does not restrict you to only passing in the properties defined in the constraint.
11541119
11551120
### Solution 5: Combining Generic Types and Functions
11561121
@@ -1205,7 +1170,9 @@ const safeFunction: <number>(func: PromiseFunc<number>) => Promise<() => Promise
12051170
12061171
The other tests pass as well.
12071172
1208-
Whatever we pass into `safeFunction` will be inferred as the type argument for `PromiseFunc`. This is because the type argument is being inferred as the identity of the generic function.
1173+
Whatever we pass into `safeFunction` will be inferred as the type argument for `PromiseFunc`. This is because the type argument is being inferred _inside_ the generic function.
1174+
1175+
This combination of generic functions and generic types can make your generic functions a lot easier to read.
12091176
12101177
### Solution 6: Multiple Type Arguments in a Generic Function
12111178
@@ -1227,6 +1194,8 @@ type PromiseFunc<TArgs extends any[], TResult> = (
12271194
) => Promise<TResult>;
12281195
```
12291196
1197+
You might have tried this with `unknown[]` - but `any[]` is the only thing that works in this scenario.
1198+
12301199
Now we need to update the `safeFunction` so that it has the same arguments as `PromiseFunc`. To do this, we'll add `TArgs` to its type parameters.
12311200
12321201
Note that we also need to update the args for the `async` function to be of type `TArgs`:
@@ -1246,32 +1215,6 @@ This change is necessary in order to make sure the function returned by `safeFun
12461215
12471216
With these changes, all of our tests pass as expected.
12481217
1249-
The big takeaway is that when you're doing inference with function parameters, you want to make sure that you're capturing the entire arguments as a tuple with `TArgs extends any[]` instead of using just an array `TArgs[]`. Otherwise, you won't get the correct type inference.
1250-
1251-
### Solution 7: Type Predicates
1252-
1253-
For the `isString` function, we know that the `input` will be a string, but TypeScript can't infer that logic on its own. To help, we can add a type predicate that says `input is string`:
1254-
1255-
```typescript
1256-
const isString = (input: unknown): input is string => {
1257-
return typeof input === "string";
1258-
};
1259-
```
1260-
1261-
With this change, the `isString` function can be used with `filter` and the test passes as expected:
1262-
1263-
```typescript
1264-
it("Should be able to be passed to .filter and work", () => {
1265-
const mixedArray = [1, "hello", [], {}];
1266-
1267-
const stringsOnly = mixedArray.filter(isString);
1268-
1269-
type test1 = Expect<Equal<typeof stringsOnly, string[]>>;
1270-
1271-
expect(stringsOnly).toEqual(["hello"]);
1272-
});
1273-
```
1274-
12751218
### Solution 8: Assertion Functions
12761219
12771220
The solution is to add a type annotation onto the return type of `assertIsAdminUser`.

0 commit comments

Comments
 (0)