Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions exercises/01.type-aliases/01.problem.naming-types/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,23 @@ clean it up with type aliases.

🐨 Open <InlineFile file="index.ts" /> and:

1. Create a `User` type alias for the user object shape
2. Create a `Product` type alias for the product object shape
3. Use these types to annotate variables and function parameters
1. Create a `User` type alias with `id`, `name`, and `email` (all `string`)
2. Create a `Product` type alias with `id`/`name` (`string`), `price`
(`number`), and `inStock` (`boolean`)
3. Create `userSample` and `productSample` values typed with those aliases
4. Implement `greet(user)` so it returns `Hello, <name>!` using the user's name
5. Implement `formatProduct(product)` so the returned string includes the product
name, the price, and a stock status that differs for in-stock vs out-of-stock
6. Export `greet`, `formatProduct`, `userSample`, and `productSample` by name

## Completion criteria

- Named exports: `greet`, `formatProduct`, `userSample`, `productSample`
- `greet({ id: '1', name: 'Alice', email: 'alice@example.com' })` returns
`Hello, Alice!`
- `formatProduct` output for an in-stock product includes its name and price,
and differs from an otherwise-equivalent out-of-stock product's output because
of stock status
- `userSample` / `productSample` field types match the aliases above

📜 [TypeScript Handbook - Type Aliases](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases)
7 changes: 5 additions & 2 deletions exercises/01.type-aliases/01.problem.naming-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

// 🐨 Create a `userSample` value using the User type

// 🐨 Create a function that takes a User and returns a greeting
// 🐨 Create a function `greet` that takes a User and returns:
// Hello, <name>! (example: Hello, Alice!)

// 🐨 Create a type alias `Product` with:
// - id: string
Expand All @@ -17,7 +18,9 @@

// 🐨 Create a `productSample` value using the Product type

// 🐨 Create a function that takes a Product and returns a formatted string
// 🐨 Create a function `formatProduct` that takes a Product and returns a string
// that includes the name, the price, and a stock status that is different for
// in-stock vs out-of-stock products (exact wording of the status is up to you)

// 🐨 Export `greet`, `formatProduct`, `userSample`, and `productSample`. Tests
// import these by name and will check their behavior and shapes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ await test('greet function should work correctly', () => {
assert.strictEqual(
solution.greet(alice),
'Hello, Alice!',
'🚨 greet function should return "Hello, Alice!" - check your function implementation',
"🚨 greet should return a greeting that uses the user's name (Hello, <name>!)",
)
})

Expand Down
19 changes: 16 additions & 3 deletions exercises/01.type-aliases/02.problem.composition/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,22 @@ self-documenting.

🐨 Open <InlineFile file="index.ts" /> and:

1. Create primitive type aliases for IDs, timestamps, and emails
2. Create `User` and `Post` types that use these building blocks
3. Create `userSample` and `postSample` values and export them (tests will import them by name)
1. Create primitive aliases `ID` (`string`), `Timestamp` (`number`), and `Email`
(`string`)
2. Create a `User` type that uses those primitives for `id`, `createdAt`,
`updatedAt`, and `email`, plus a `name: string` field
3. Create a `Post` type that uses those primitives for `id`, `createdAt`,
`updatedAt`, and `authorId`, plus `title` and `content` string fields
4. Create `userSample` and `postSample` values that match those types and export
them by name

## Completion criteria

- Named exports: `userSample`, `postSample`
- `userSample` has string `id` / `name` / `email` and number `createdAt` /
`updatedAt`
- `postSample` has string `id` / `title` / `content` / `authorId` and number
`createdAt` / `updatedAt`

💰 Compose larger types from your primitive aliases.

Expand Down
19 changes: 10 additions & 9 deletions exercises/01.type-aliases/02.problem.composition/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
// Composing Types from Building Blocks

// 🐨 Create primitive type aliases named `ID`, `Timestamp`, and `Email`.
// 💰 Underlying types: ID → string, Timestamp → number, Email → string

// 🐨 Create a `User` type that uses those primitives for:
// - id
// - createdAt
// - updatedAt
// - email
// - id (ID)
// - createdAt (Timestamp)
// - updatedAt (Timestamp)
// - email (Email)
// and includes a `name` string field.

// 🐨 Create a `Post` type that uses those primitives for:
// - id
// - createdAt
// - updatedAt
// - authorId
// - id (ID)
// - createdAt (Timestamp)
// - updatedAt (Timestamp)
// - authorId (ID)
// and includes `title` and `content` string fields.

// 🐨 Create a `userSample` and a `postSample` value that match your types.
// 🐨 Export `userSample` and `postSample`. Tests will import these by name and
// verify the fields have the expected types.
// verify the fields have the expected runtime types (string vs number).
// export { userSample, postSample }
14 changes: 11 additions & 3 deletions exercises/01.type-aliases/03.problem.record-type/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ gives us a clean way to describe those object shapes.

🐨 Open <InlineFile file="index.ts" /> and:

1. Create a `UsersById` type using `Record`
2. Create a `RoleCounts` type using `Record`
3. Create objects that match those types
1. Create a `UsersById` type using `Record` that maps string IDs to `User`
2. Create a `RoleCounts` type using `Record` that maps role strings to numbers
3. Type the provided `usersById` and `roleCounts` objects with those aliases
(remove the `@ts-expect-error` comments once the types exist)
4. Export `usersById` and `roleCounts` by name

## Completion criteria

- Named exports: `usersById`, `roleCounts`
- `usersById.u1.name` is `'Ava'` and `usersById.u2.role` is `'member'`
- `roleCounts` deep-equals `{ admin: 1, member: 1 }`

📜 [TypeScript Handbook - Record](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type)
11 changes: 4 additions & 7 deletions exercises/01.type-aliases/03.problem.record-type/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,23 @@ const users: Array<User> = [
{ id: 'u2', name: 'Ben', role: 'member' },
]

// 🐨 Create a UsersById type using Record to map IDs to User
// 🦺 Use string keys for IDs
// 🐨 Create a UsersById type using Record to map string IDs to User

// 🐨 Create a RoleCounts type using Record to map role strings to numbers
// 🦺 Use string keys for the role names

// 🐨 Create a usersById object that matches UsersById
// 🐨 Type this usersById object as UsersById (remove @ts-expect-error when ready)
// @ts-expect-error - 💣 remove this comment when you create UsersById
const usersById: UsersById = {
u1: users[0],
u2: users[1],
}

// 🐨 Create a roleCounts object that matches RoleCounts
// 🐨 Type this roleCounts object as RoleCounts (remove @ts-expect-error when ready)
// @ts-expect-error - 💣 remove this comment when you create RoleCounts
const roleCounts: RoleCounts = {
admin: 1,
member: 1,
}

// 🐨 Export `usersById` and `roleCounts`. Tests import these by name and check
// that the values match your Record-based types.
// 🐨 Export `usersById` and `roleCounts`. Tests import these by name.
// export { usersById, roleCounts }
18 changes: 15 additions & 3 deletions exercises/02.union-types/01.problem.multiple-types/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ this.

🐨 Open <InlineFile file="index.ts" /> and:

1. Create a type for values that can be string or number
2. Create a type for IDs that can be string or number
3. Write functions that handle union types
1. Create a type `ID` that is `string | number`
2. Implement `formatId(id: ID): string` — number IDs get a `#` prefix; string IDs
are returned unchanged (including `0` and `''`)
3. Create a type `Result` that is `string | Error`
4. Implement `processResult(result: Result): string` — strings become
`Success: <value>`; Errors become `Error: <message>`
5. Export `formatId` and `processResult` by name

## Completion criteria

- Named exports: `formatId`, `processResult`
- `formatId(123)` → `'#123'`; `formatId('abc')` → `'abc'`; `formatId(0)` →
`'#0'`; `formatId('')` → `''`
- `processResult('Done!')` → `'Success: Done!'`
- `processResult(new Error('Oops'))` → `'Error: Oops'`

<callout-info>
`typeof` checks if a value is a string, number, boolean, or symbol. For
Expand Down
14 changes: 7 additions & 7 deletions exercises/02.union-types/01.problem.multiple-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

// 🐨 Create a type `ID` that can be string or number

// 🐨 Create a function `formatId` that takes an ID and returns a string
// If it's a number, prefix with '#'
// If it's a string, return as-is
// 🐨 Create a function `formatId(id: ID): string`
// - numberprefix with '#' (including 0 → '#0')
// - stringreturn as-is (including '')

// console.log(formatId(123)) // "#123"
// console.log(formatId('abc')) // "abc"

// 🐨 Create a type `Result` that can be string (success) or Error (failure)

// 🐨 Create a function `processResult` that takes a Result
// If it's a string, return "Success: [value]"
// If it's an Error, return "Error: [message]"
// 💰 Check the type before accessing properties
// 🐨 Create a function `processResult(result: Result): string`
// - string → `Success: ${value}`
// - Error → `Error: ${message}`
// 💰 Narrow before reading Error.message

// console.log(processResult('Done!')) // "Success: Done!"
// console.log(processResult(new Error('Oops'))) // "Error: Oops"
Expand Down
24 changes: 21 additions & 3 deletions exercises/02.union-types/02.problem.narrowing/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,27 @@ function process(value: string | Array<string>) {

🐨 Open <InlineFile file="index.ts" /> and:

1. Write a function that handles `string | Array<string>`
2. Write a function that handles objects with different shapes
3. Use the `in` operator to check for properties
1. Implement `normalizeText` for the exported `TextInput` type
- string → trimmed
- array → join with spaces, then trim the joined result
2. Implement `describeUser` for the exported `User` type by checking which
properties exist
- admin (`permissions`) → Admin with N permissions (N is the count)
- regular (`subscription`) → Regular user (subscription value)
- guest → Guest user
3. Export `normalizeText` and `describeUser` by name

## Completion criteria

- Named exports: `normalizeText`, `describeUser`
- `normalizeText(' hello ')` → `hello`
- `normalizeText(['hello', 'world'])` → `hello world`
- `normalizeText([' hello ', ' world '])` → `hello world` (join first,
then trim only the outer edges)
- `describeUser({ permissions: ['read', 'write'] })` →
`Admin with 2 permissions`
- `describeUser({ subscription: 'free' })` → `Regular user (free)`
- `describeUser({ guestCode: 'G-001' })` → `Guest user`

📜 [TypeScript Handbook - Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)

Expand Down
10 changes: 5 additions & 5 deletions exercises/02.union-types/02.problem.narrowing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export type TextInput = string | Array<string>

// 🐨 Create a function `normalizeText` that:
// - If string, returns it trimmed
// - If array, joins with spaces and trims
// 💰 Check the type before processing
// - If array, joins with spaces and then trims the joined string
// 💰 Narrow before processing each branch

// console.log(normalizeText(' hello '))
// console.log(normalizeText(['hello', 'world']))
Expand All @@ -19,9 +19,9 @@ export type User = AdminUser | RegularUser | GuestUser

// 🐨 Create a function `describeUser` that returns a description
// Narrow by checking which properties exist
// - Admin: "Admin with X permissions"
// - Regular: "Regular user (subscription)"
// - Guest: "Guest user"
// - Admin: Admin with <permission count> permissions
// - Regular: Regular user (<subscription>)
// - Guest: Guest user

// const admin: User = { permissions: ['read', 'write'] }
// console.log(describeUser(admin))
Expand Down
17 changes: 14 additions & 3 deletions exercises/02.union-types/03.problem.type-guards/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,19 @@ function speak(pet: Pet) {

🐨 Open <InlineFile file="index.ts" /> and:

1. Create type guards for `TextInput` and use them in `normalizeText`
2. Create type guards for each `User` variant
3. Use the type guards to implement `describeUser`
1. Create an `isStringArray` type guard and use it inside `normalizeText`
2. Create `isAdminUser`, `isRegularUser`, and `isGuestUser` type guards and use
them in `describeUser`
3. Export `isStringArray`, `normalizeText`, `isAdminUser`, `isRegularUser`,
`isGuestUser`, and `describeUser` by name

## Completion criteria

- Named exports listed above
- `isStringArray` is true for string arrays and false for strings / number arrays
- User guards are true only for their own shape
- `normalizeText` / `describeUser` keep the same exact outputs as the Narrowing
step (for example `Admin with 2 permissions`, `Regular user (free)`,
`Guest user`)

📜 [TypeScript Handbook - Type Guards](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
18 changes: 9 additions & 9 deletions exercises/02.union-types/03.problem.type-guards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

export type TextInput = string | Array<string>

// 🐨 Create a type guard `isStringArray` that checks if a value is an array of strings
// 💰 Use Array.isArray and verify each item is a string
// 🐨 Create a type guard `isStringArray` for arrays of strings
// 💰 Confirm it's an array and every item is a string

// 🐨 update this function to use the type guard
// 🐨 Update `normalizeText` to use `isStringArray`
function normalizeText(input: TextInput) {
if (Array.isArray(input)) {
return input.join(' ').trim()
Expand All @@ -20,13 +20,13 @@ type GuestUser = { guestCode: string }

export type User = AdminUser | RegularUser | GuestUser

// 🐨 Create type guards:
// - `isAdminUser` (permissions array)
// - `isRegularUser` (subscription string)
// - `isGuestUser` (guestCode string)
// 💰 These should return `value is ...` so TypeScript narrows
// 🐨 Create type guards for each User variant:
// - `isAdminUser` permissions array of strings
// - `isRegularUser` subscription free or premium
// - `isGuestUser` string guestCode
// 💰 Accept unknown input; return false for non-matching shapes

// 🐨 update this function to use the type guards
// 🐨 Update `describeUser` to use the type guards (same output strings as before)
function describeUser(user: User) {
if ('permissions' in user) {
return `Admin with ${user.permissions.length} permissions`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,28 @@ create valid combinations.

🐨 Open <InlineFile file="index.ts" /> and:

1. Create discriminated unions for API responses
2. Create discriminated unions for payment methods
3. Handle all cases with exhaustiveness checking
1. Replace the placeholder `ApiState` with a discriminated union on `status`.
Valid variants (each exclusive — a loading state must not require success or
error fields, and so on):
- loading — only the loading discriminant
- success — string-array `data`
- error — string `error`
2. Replace the placeholder `PaymentMethod` with a discriminated union on `type`.
Valid variants (each exclusive):
- credit card — `last4` and `expiry` strings
- PayPal — `email` string
- bank — `accountNumber` string
3. Remove the `@ts-expect-error` comments once the unions are correct so the
`never` exhaustiveness checks type-check
4. Export `renderState` and `describePayment` by name (their bodies are already
written)

## Completion criteria

- Named exports: `renderState`, `describePayment`
- `renderState` / `describePayment` keep working for each variant using the
return strings already in the starter switch cases
- Invalid mixed shapes (for example success fields on a loading state) are not
representable

📜 [TypeScript Handbook - Discriminated Unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)
Loading
Loading