Skip to content

Commit cad637c

Browse files
✨ feat: add Type Operations > Array Type Shenanigans project (#87)
1 parent 3314cd1 commit cad637c

34 files changed

+362
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
*.js
3+
*.tsbuildinfo
34
!*.config.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Step 1: Flat Filter
2+
3+
_Learning TypeScript_ introduced a generic `ArrayItemsRecursive` type that retrieves the nested elements from any input array type.
4+
It works by using an inferred conditional type to check if the input is an array, and recursing if so:
5+
6+
```ts
7+
type ArrayItemsRecursive<T> = T extends (infer Item)[]
8+
? ArrayItemsRecursive<Item>
9+
: T;
10+
11+
// Type: string
12+
type String2DItem = ArrayItemsRecursive<string[][]>;
13+
```
14+
15+
Another trick you can do with generic type parameters is using `extends` to check if the parameter matches some other type.
16+
For example, this `IdentityFilter` type returns an original type `T` only if it `extends Filter`:
17+
18+
```ts
19+
type IdentityFilter<T, Filter> = T extends Filter ? T : never;
20+
```
21+
22+
For this step, use that `extends` filter concept with an array flattener type to both flatten an array type and filter the flattened items.
23+
24+
## Specification
25+
26+
Write a `FilteredArrayItems` type that takes in two type parameters:
27+
28+
1. `T`
29+
2. `Filter`
30+
31+
It should result in a flattened array or tuple type of items that only `extend Filter`.
32+
33+
## Examples
34+
35+
- `FilteredArrayItems<number[], string>` -> `number`
36+
- `FilteredArrayItems<(number | string)[], number>` -> `number`
37+
- `FilteredArrayItems<["a", 1, "b", 2], string>` -> `"a" | "b"`
38+
39+
## Files
40+
41+
- `index.ts`: Write your `FilteredArrayItems` type here
42+
- `solution.ts`: Solution code

projects/type-operations/array-type-shenanigans/01-flat-filter/index.d.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { FilteredArrayItems } from "~/index";
2+
3+
function expectTypeExtends<T, U extends T>() {}
4+
5+
expectTypeExtends<never, FilteredArrayItems<[], string>>();
6+
7+
expectTypeExtends<never, FilteredArrayItems<number, string>>();
8+
9+
expectTypeExtends<never, FilteredArrayItems<number[], string>>();
10+
11+
expectTypeExtends<number, FilteredArrayItems<(number | string)[], number>>();
12+
13+
expectTypeExtends<"a", FilteredArrayItems<["a"], string>>();
14+
15+
expectTypeExtends<string, FilteredArrayItems<["a", string], string>>();
16+
17+
expectTypeExtends<never, FilteredArrayItems<[1], string>>();
18+
19+
expectTypeExtends<never, FilteredArrayItems<[1, number], string>>();
20+
21+
expectTypeExtends<"a", FilteredArrayItems<[1, "a", 2, number], string>>();
22+
23+
expectTypeExtends<number, FilteredArrayItems<[1, "a", 2, number], number>>();
24+
25+
expectTypeExtends<"a", FilteredArrayItems<[1, "a", 2, 3], string>>();
26+
27+
expectTypeExtends<1 | 2 | 3, FilteredArrayItems<[1, "a", 2, 3], number>>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Write your FilteredArrayItems type here! ✨
2+
// You'll need to export it so the tests can run it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export declare type FilteredArrayItems<T, Filter> = T extends (infer Item)[]
2+
? Item extends Filter
3+
? FilteredArrayItems<Item, Filter>
4+
: never
5+
: T extends Filter
6+
? T
7+
: never;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type FilteredArrayItems<T, Filter> = T extends (infer Item)[]
2+
? Item extends Filter
3+
? FilteredArrayItems<Item, Filter>
4+
: never
5+
: T extends Filter
6+
? T
7+
: never;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"~/index": ["./index.ts"]
6+
}
7+
},
8+
"include": ["./index.test.ts"]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"~/index": ["./solution.ts"]
6+
}
7+
},
8+
"include": ["./index.test.ts"]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Step 2: Reverse
2+
3+
Next up, let's have you start writing your own conditional types from scratch.
4+
You'll be working with `...` spread and rest operations, which conditional types are able to use on types known to be array types.
5+
You can enforce that a type parameter is an array type by giving it an array constraint like `extends any[]`.
6+
7+
This `ArrayLength` type ensures `T` is some kind of array, so `T` is guaranteed to have a `length` property:
8+
9+
```ts
10+
type ArrayLength<T extends any[]> = T["length"];
11+
12+
// Type: number
13+
type LengthOfStringArray = ArrayLength<string[]>;
14+
15+
// Type: 3
16+
type LengthOfTupleTrio = ArrayLength<["a", "b", "c"]>;
17+
```
18+
19+
Spreads on array types allow types to _combine_ array types.
20+
For example, this `Unshift` type adds an item `Item` to the beginning of the `Rest` array:
21+
22+
```ts
23+
type Unshift<Rest extends any[], Item> = [Item, ...Rest];
24+
25+
// Type: ["First!", "a", "b", "c"]
26+
type PrefixFirst = Unshift<["a", "b", "c"], "First!">;
27+
```
28+
29+
Rests on array types allow types to _extract_ from array types.
30+
For example, this `Pop` type ignores the first element in the array type retrieves the rest as `Rest`:
31+
32+
```ts
33+
type Pop<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
34+
35+
// Type: ["b", "c"]
36+
type PopFirst = Pop<["a", "b", "c"]>;
37+
```
38+
39+
Your task for this step is to use both concepts to create a recursive `Reverse` type.
40+
`Reverse` should take in an array as a type parameter `T` and return an array that consists of:
41+
42+
1. The first element in `T`
43+
2. ...a rest of the result of `Reverse` on the remaining elements of `T`
44+
45+
> Fans of functional programming may recognize this as a very functional way of reversing a list!
46+
47+
## Specification
48+
49+
Write a `Reverse` type that takes in one type parameter that must be an array: `T`.
50+
51+
It should result in a reversed array or tuple type of `T`'s elements.
52+
53+
## Examples
54+
55+
- `Reverse<number[]>` -> `number[]`
56+
- `Reverse<(number | string)[]>` -> `(number | string)[]` (which may also be written as `(string | number)[]`)
57+
- `Reverse<['a', 'b', 'c']>` -> `['c', 'b', 'a']`
58+
59+
## Files
60+
61+
- `index.ts`: Write your `Reverse` type here
62+
- `solution.ts`: Solution code

projects/type-operations/array-type-shenanigans/02-reverse/index.d.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Reverse } from "~/index";
2+
3+
function expectTypeExtends<T, U extends T>() {}
4+
5+
expectTypeExtends<[], Reverse<[]>>();
6+
7+
expectTypeExtends<[number], Reverse<[number]>>();
8+
9+
expectTypeExtends<[1, number], Reverse<[number, 1]>>();
10+
11+
expectTypeExtends<["b", string, "a"], Reverse<["a", string, "b"]>>();
12+
13+
expectTypeExtends<string[], Reverse<string[]>>();
14+
15+
expectTypeExtends<(number | string)[], Reverse<(number | string)[]>>();
16+
17+
expectTypeExtends<[["a", ["b"]], number], Reverse<[number, ["a", ["b"]]]>>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Write your Reverse type here! ✨
2+
// You'll need to export it so the tests can run it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export declare type Reverse<T extends any[]> = T extends [
2+
infer First,
3+
...infer Rest
4+
]
5+
? [...Reverse<Rest>, First]
6+
: T;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type Reverse<T extends any[]> = T extends [infer First, ...infer Rest]
2+
? [...Reverse<Rest>, First]
3+
: T;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"~/index": ["./index.ts"]
6+
}
7+
},
8+
"include": ["./index.test.ts"]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"~/index": ["./solution.ts"]
6+
}
7+
},
8+
"include": ["./index.test.ts"]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Step 3: Zip
2+
3+
Let's crank things up a little bit with array spreads and rests.
4+
This step won't introduce any new concepts.
5+
You'll be using the same concepts from the last two steps more.
6+
7+
Write a `Zip` type that takes in two array types, `T` and `U`.
8+
It should "zip" them together akin to the behavior of [Lodash's `zip`](https://lodash.com/docs/4.17.15#zip).
9+
10+
## Examples
11+
12+
- `Zip<["a"], [1]>` -> `["a", 1]`
13+
- `Zip<["a", "b"], [1]>` -> `["a", 1, "b"]`
14+
- `Zip<["a", "b"], [1, 2, 3]>` -> `["a", 1, "b", 2, 3]`
15+
16+
## Files
17+
18+
- `index.ts`: Write your `Zip` type here
19+
- `solution.ts`: Solution code

projects/type-operations/array-type-shenanigans/03-zip/index.d.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Zip } from "~/index";
2+
3+
function expectTypeExtends<T, U extends T>() {}
4+
5+
expectTypeExtends<[], Zip<[], []>>();
6+
7+
expectTypeExtends<[number], Zip<[number], []>>();
8+
9+
expectTypeExtends<[number, string], Zip<[number], [string]>>();
10+
11+
expectTypeExtends<[string], Zip<[], [string]>>();
12+
13+
expectTypeExtends<["a", 1, 2, 3], Zip<["a"], [1, 2, 3]>>();
14+
15+
expectTypeExtends<["a", 1, "b", 2, 3], Zip<["a", "b"], [1, 2, 3]>>();
16+
17+
expectTypeExtends<["a", 1, "b", 2, "c", 3], Zip<["a", "b", "c"], [1, 2, 3]>>();
18+
19+
expectTypeExtends<
20+
["a", 1, "b", 2, "c", 3, "d"],
21+
Zip<["a", "b", "c", "d"], [1, 2, 3]>
22+
>();
23+
24+
expectTypeExtends<
25+
["a", 1, "b", 2, "c", 3, "d", "e"],
26+
Zip<["a", "b", "c", "d", "e"], [1, 2, 3]>
27+
>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Write your Zip type here! ✨
2+
// You'll need to export it so the tests can run it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export declare type Zip<T extends any[], U extends any[]> = T extends [
2+
infer FirstT,
3+
...infer RestT
4+
]
5+
? U extends [infer FirstU, ...infer RestU]
6+
? [FirstT, FirstU, ...Zip<RestT, RestU>]
7+
: [...T]
8+
: U;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type Zip<T extends any[], U extends any[]> = T extends [
2+
infer FirstT,
3+
...infer RestT
4+
]
5+
? U extends [infer FirstU, ...infer RestU]
6+
? [FirstT, FirstU, ...Zip<RestT, RestU>]
7+
: [...T]
8+
: U;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"~/index": ["./index.ts"]
6+
}
7+
},
8+
"include": ["./index.test.ts"]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"~/index": ["./solution.ts"]
6+
}
7+
},
8+
"include": ["./index.test.ts"]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Array Type Shenanigans
2+
3+
> A [Learning TypeScript > Type Operations](https://learning-typescript.com/type-operations) 🥗 appetizer project.
4+
5+
Hi!
6+
This is Josh, author of _Learning TypeScript_.
7+
Congrats on making it all the way through the book to the final chapter!
8+
Type operations are a ton of fun to play around with once you get the hang of them.
9+
10+
These appetizer projects showcase a bunch of my favorite cool things you can do using array and/or tuple types with conditional types.
11+
Each gives a small into to the new thing because it can be tricky to find out the ways you can or cannot combine type operations.
12+
There's no silly theme, to help not distract from learning those new things.
13+
14+
I hope you're enjoying the book and these projects! 💖
15+
16+
## Setup
17+
18+
Start the TypeScript compiler in watch mode:
19+
20+
```shell
21+
tsc --watch
22+
```
23+
24+
## Steps
25+
26+
- [1. Flat Filter](./01-flat-filter)
27+
- [2. Reverse](./02-reverse)
28+
- [3. Zip](./03-zip)
29+
30+
## Notes
31+
32+
- Don't import code from one step into another.
33+
- You'll be working entirely in the type system. There will be no runtime code. 🤘
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"label": "🥗 Array Type Shenanigans",
3+
"position": 1
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "type-shenanigans",
3+
"scripts": {
4+
"test": "tsc",
5+
"test:solutions": "tsc -p ./tsconfig.test.json"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true
4+
},
5+
"references": [
6+
{ "path": "./01-flat-filter/tsconfig.json" },
7+
{ "path": "./02-reverse/tsconfig.json" },
8+
{ "path": "./03-zip/tsconfig.json" }
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true
4+
},
5+
"references": [
6+
{ "path": "./01-flat-filter/tsconfig.test.json" },
7+
{ "path": "./02-reverse/tsconfig.test.json" },
8+
{ "path": "./03-zip/tsconfig.test.json" }
9+
]
10+
}

0 commit comments

Comments
 (0)