diff --git a/exercises/01.type-aliases/01.problem.naming-types/README.mdx b/exercises/01.type-aliases/01.problem.naming-types/README.mdx index 9585042..c21472c 100644 --- a/exercises/01.type-aliases/01.problem.naming-types/README.mdx +++ b/exercises/01.type-aliases/01.problem.naming-types/README.mdx @@ -7,8 +7,23 @@ clean it up with type aliases. 🐨 Open 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, !` 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) diff --git a/exercises/01.type-aliases/01.problem.naming-types/index.ts b/exercises/01.type-aliases/01.problem.naming-types/index.ts index ecf3e25..76afc7d 100644 --- a/exercises/01.type-aliases/01.problem.naming-types/index.ts +++ b/exercises/01.type-aliases/01.problem.naming-types/index.ts @@ -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, ! (example: Hello, Alice!) // 🐨 Create a type alias `Product` with: // - id: string @@ -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. diff --git a/exercises/01.type-aliases/01.solution.naming-types/index.test.ts b/exercises/01.type-aliases/01.solution.naming-types/index.test.ts index 0fda283..574413d 100644 --- a/exercises/01.type-aliases/01.solution.naming-types/index.test.ts +++ b/exercises/01.type-aliases/01.solution.naming-types/index.test.ts @@ -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, !)", ) }) diff --git a/exercises/01.type-aliases/02.problem.composition/README.mdx b/exercises/01.type-aliases/02.problem.composition/README.mdx index ef1a9a6..8f3dd5e 100644 --- a/exercises/01.type-aliases/02.problem.composition/README.mdx +++ b/exercises/01.type-aliases/02.problem.composition/README.mdx @@ -17,9 +17,22 @@ self-documenting. 🐨 Open 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. diff --git a/exercises/01.type-aliases/02.problem.composition/index.ts b/exercises/01.type-aliases/02.problem.composition/index.ts index 6a22387..1e8616a 100644 --- a/exercises/01.type-aliases/02.problem.composition/index.ts +++ b/exercises/01.type-aliases/02.problem.composition/index.ts @@ -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 } diff --git a/exercises/01.type-aliases/03.problem.record-type/README.mdx b/exercises/01.type-aliases/03.problem.record-type/README.mdx index c324260..c309c27 100644 --- a/exercises/01.type-aliases/03.problem.record-type/README.mdx +++ b/exercises/01.type-aliases/03.problem.record-type/README.mdx @@ -7,8 +7,16 @@ gives us a clean way to describe those object shapes. 🐨 Open 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) diff --git a/exercises/01.type-aliases/03.problem.record-type/index.ts b/exercises/01.type-aliases/03.problem.record-type/index.ts index 5ac5285..96f33fe 100644 --- a/exercises/01.type-aliases/03.problem.record-type/index.ts +++ b/exercises/01.type-aliases/03.problem.record-type/index.ts @@ -12,26 +12,23 @@ const users: Array = [ { 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 } diff --git a/exercises/02.union-types/01.problem.multiple-types/README.mdx b/exercises/02.union-types/01.problem.multiple-types/README.mdx index d2d0455..1f7e648 100644 --- a/exercises/02.union-types/01.problem.multiple-types/README.mdx +++ b/exercises/02.union-types/01.problem.multiple-types/README.mdx @@ -7,9 +7,21 @@ this. 🐨 Open 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: `; Errors become `Error: ` +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'` `typeof` checks if a value is a string, number, boolean, or symbol. For diff --git a/exercises/02.union-types/01.problem.multiple-types/index.ts b/exercises/02.union-types/01.problem.multiple-types/index.ts index 7fdf577..ac660cd 100644 --- a/exercises/02.union-types/01.problem.multiple-types/index.ts +++ b/exercises/02.union-types/01.problem.multiple-types/index.ts @@ -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` +// - number β†’ prefix with '#' (including 0 β†’ '#0') +// - string β†’ return 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" diff --git a/exercises/02.union-types/02.problem.narrowing/README.mdx b/exercises/02.union-types/02.problem.narrowing/README.mdx index b05f6dd..e94d027 100644 --- a/exercises/02.union-types/02.problem.narrowing/README.mdx +++ b/exercises/02.union-types/02.problem.narrowing/README.mdx @@ -18,9 +18,27 @@ function process(value: string | Array) { 🐨 Open and: -1. Write a function that handles `string | Array` -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) diff --git a/exercises/02.union-types/02.problem.narrowing/index.ts b/exercises/02.union-types/02.problem.narrowing/index.ts index 9ac86b6..e6a17f8 100644 --- a/exercises/02.union-types/02.problem.narrowing/index.ts +++ b/exercises/02.union-types/02.problem.narrowing/index.ts @@ -4,8 +4,8 @@ export type TextInput = string | Array // 🐨 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'])) @@ -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 permissions +// - Regular: Regular user () +// - Guest: Guest user // const admin: User = { permissions: ['read', 'write'] } // console.log(describeUser(admin)) diff --git a/exercises/02.union-types/03.problem.type-guards/README.mdx b/exercises/02.union-types/03.problem.type-guards/README.mdx index 4a5236c..60cc8e6 100644 --- a/exercises/02.union-types/03.problem.type-guards/README.mdx +++ b/exercises/02.union-types/03.problem.type-guards/README.mdx @@ -31,8 +31,19 @@ function speak(pet: Pet) { 🐨 Open 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) diff --git a/exercises/02.union-types/03.problem.type-guards/index.ts b/exercises/02.union-types/03.problem.type-guards/index.ts index 9f7e83a..d2ba654 100644 --- a/exercises/02.union-types/03.problem.type-guards/index.ts +++ b/exercises/02.union-types/03.problem.type-guards/index.ts @@ -2,10 +2,10 @@ export type TextInput = string | Array -// 🐨 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() @@ -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` diff --git a/exercises/02.union-types/04.problem.discriminated-unions/README.mdx b/exercises/02.union-types/04.problem.discriminated-unions/README.mdx index c1ddd56..6d3ede1 100644 --- a/exercises/02.union-types/04.problem.discriminated-unions/README.mdx +++ b/exercises/02.union-types/04.problem.discriminated-unions/README.mdx @@ -18,8 +18,28 @@ create valid combinations. 🐨 Open 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) diff --git a/exercises/02.union-types/04.problem.discriminated-unions/index.ts b/exercises/02.union-types/04.problem.discriminated-unions/index.ts index 340da40..2fab836 100644 --- a/exercises/02.union-types/04.problem.discriminated-unions/index.ts +++ b/exercises/02.union-types/04.problem.discriminated-unions/index.ts @@ -1,10 +1,10 @@ // Discriminated Unions Pattern -// 🐨 Create discriminated union types for API response states: -// - Loading: status "loading" -// - Success: status "success" with data: Array -// - Error: status "error" with error: string -// πŸ’° Replace the placeholder ApiState with a real discriminated union +// 🐨 Replace ApiState with a discriminated union on `status`. +// Valid exclusive variants: +// - loading (no data/error fields) +// - success (includes string-array data) +// - error (includes string error) type ApiState = { status: string @@ -12,12 +12,9 @@ type ApiState = { error: string } -// const loadingState: ApiState = { status: 'loading' } -// console.log(renderState(loadingState)) -// const successState: ApiState = { status: 'success', data: ['a', 'b'] } -// console.log(renderState(successState)) -// const errorState: ApiState = { status: 'error', error: 'Network error' } -// console.log(renderState(errorState)) +// console.log(renderState(/* a loading ApiState */)) +// console.log(renderState(/* a success ApiState with data */)) +// console.log(renderState(/* an error ApiState */)) function renderState(state: ApiState): string { switch (state.status) { @@ -35,11 +32,11 @@ function renderState(state: ApiState): string { } } -// 🐨 Create a discriminated union for payment methods: -// - Credit card: type "credit_card" with last4 and expiry strings -// - PayPal: type "paypal" with email string -// - Bank transfer: type "bank" with accountNumber string -// πŸ’° Replace the placeholder PaymentMethod with a real discriminated union +// 🐨 Replace PaymentMethod with a discriminated union on `type`. +// Valid exclusive variants: +// - credit card (last4 + expiry strings) +// - PayPal (email string) +// - bank transfer (accountNumber string) type PaymentMethod = { type: string @@ -49,12 +46,9 @@ type PaymentMethod = { accountNumber: string } -// const card: PaymentMethod = { type: 'credit_card', last4: '1234', expiry: '12/25' } -// console.log(describePayment(card)) -// const paypal: PaymentMethod = { type: 'paypal', email: 'me@example.com' } -// console.log(describePayment(paypal)) -// const bank: PaymentMethod = { type: 'bank', accountNumber: '000123' } -// console.log(describePayment(bank)) +// console.log(describePayment(/* a credit card PaymentMethod */)) +// console.log(describePayment(/* a PayPal PaymentMethod */)) +// console.log(describePayment(/* a bank PaymentMethod */)) function describePayment(method: PaymentMethod): string { switch (method.type) { diff --git a/exercises/02.union-types/04.solution.discriminated-unions/index.test.ts b/exercises/02.union-types/04.solution.discriminated-unions/index.test.ts index 7aca9d1..4ac23e2 100644 --- a/exercises/02.union-types/04.solution.discriminated-unions/index.test.ts +++ b/exercises/02.union-types/04.solution.discriminated-unions/index.test.ts @@ -20,7 +20,7 @@ await test('renderState should handle loading state', () => { assert.strictEqual( solution.renderState({ status: 'loading' }), 'Loading...', - '🚨 renderState should return "Loading..." for loading state - check your discriminated union narrowing', + '🚨 renderState should report the loading state - check your discriminated union narrowing', ) }) diff --git a/exercises/03.literal-types/01.problem.preserve-literals/README.mdx b/exercises/03.literal-types/01.problem.preserve-literals/README.mdx index ff16f6c..b30fc7b 100644 --- a/exercises/03.literal-types/01.problem.preserve-literals/README.mdx +++ b/exercises/03.literal-types/01.problem.preserve-literals/README.mdx @@ -7,10 +7,19 @@ TypeScript widens the booleans to `boolean`, which loses useful information. 🐨 Open and: -1. Add `as const` to the `roles` object -2. Create a `Role` type using `keyof typeof roles` -3. Fix the `adminCanDelete` assignment -4. Implement `canDeleteUsers` using the `roles` data +1. Add `as const` to the `roles` object so `roles.admin.canDeleteUsers` stays the + literal type `true` +2. Create a `Role` type from the keys of `roles` +3. Remove the `@ts-expect-error` once `adminCanDelete: true` type-checks +4. Implement `canDeleteUsers(role: Role): boolean` using the `roles` data (admin + β†’ `true`, editor/viewer β†’ `false`) +5. Export `roles`, `adminCanDelete`, and `canDeleteUsers` by name + +## Completion criteria + +- Named exports: `roles`, `adminCanDelete`, `canDeleteUsers` +- `canDeleteUsers('admin')` is `true`; `'editor'` and `'viewer'` are `false` +- With `as const`, `adminCanDelete` can be annotated as the literal `true` πŸ’° `as const` preserves `true` and `false` as literal types, not just `boolean`. diff --git a/exercises/03.literal-types/01.problem.preserve-literals/index.ts b/exercises/03.literal-types/01.problem.preserve-literals/index.ts index 79d2109..8fc2f24 100644 --- a/exercises/03.literal-types/01.problem.preserve-literals/index.ts +++ b/exercises/03.literal-types/01.problem.preserve-literals/index.ts @@ -15,11 +15,11 @@ const roles = { // 🐨 Create a `Role` type from the keys of `roles` -// 🐨 Add `as const` to the `roles` object so this becomes `true` +// 🐨 Add `as const` to the `roles` object so this becomes the literal `true` // @ts-expect-error - πŸ’£ remove this comment const adminCanDelete: true = roles.admin.canDeleteUsers -// 🐨 Implement `canDeleteUsers` to return whether a role can delete users +// 🐨 Implement `canDeleteUsers(role: Role): boolean` using the roles data // 🐨 Export `roles`, `adminCanDelete`, and `canDeleteUsers` for tests. // export { roles, adminCanDelete, canDeleteUsers } diff --git a/exercises/03.literal-types/02.problem.single-source-of-truth/README.mdx b/exercises/03.literal-types/02.problem.single-source-of-truth/README.mdx index cea3c18..b44868f 100644 --- a/exercises/03.literal-types/02.problem.single-source-of-truth/README.mdx +++ b/exercises/03.literal-types/02.problem.single-source-of-truth/README.mdx @@ -7,10 +7,19 @@ Using `as const` lets us derive types directly from the runtime object. 🐨 Open and: -1. Add `as const` to the `routes` object -2. Create `RouteName` and `RoutePath` types from the object -3. Fix the `defaultRoute` assignment -4. Implement `getRoutePath` to return the route path for a given name +1. Add `as const` to the `routes` object so `routes.home` stays the literal `'/'` +2. Create `RouteName` from the keys of `routes` and `RoutePath` from the values +3. Remove the `@ts-expect-error` once `defaultRoute: '/'` type-checks +4. Implement `getRoutePath(name: RouteName): RoutePath` so it returns the path + for that name from `routes` +5. Export `routes`, `defaultRoute`, and `getRoutePath` by name + +## Completion criteria + +- Named exports: `routes`, `defaultRoute`, `getRoutePath` +- `getRoutePath('home')` β†’ `'/'` +- `getRoutePath('login')` β†’ `'/login'` +- `getRoutePath('settings')` β†’ `'/settings'` πŸ’° `keyof typeof` gives you the keys; indexed access gives you the values. diff --git a/exercises/03.literal-types/02.problem.single-source-of-truth/index.ts b/exercises/03.literal-types/02.problem.single-source-of-truth/index.ts index 97bb3b6..97c7c4d 100644 --- a/exercises/03.literal-types/02.problem.single-source-of-truth/index.ts +++ b/exercises/03.literal-types/02.problem.single-source-of-truth/index.ts @@ -4,7 +4,7 @@ const routes = { settings: '/settings', } -// 🐨 Create a `RouteName` type using `keyof typeof routes` +// 🐨 Create a `RouteName` type from the keys of `routes` // 🐨 Create a `RoutePath` type from the values of `routes` @@ -12,7 +12,7 @@ const routes = { // @ts-expect-error - πŸ’£ remove this comment const defaultRoute: '/' = routes.home -// 🐨 Implement `getRoutePath` to return the path for a route name +// 🐨 Implement `getRoutePath(name: RouteName): RoutePath` using `routes` // 🐨 Export `routes`, `defaultRoute`, and `getRoutePath` for tests. // export { routes, defaultRoute, getRoutePath } diff --git a/exercises/03.literal-types/02.solution.single-source-of-truth/index.test.ts b/exercises/03.literal-types/02.solution.single-source-of-truth/index.test.ts index 5850a15..b18bd1c 100644 --- a/exercises/03.literal-types/02.solution.single-source-of-truth/index.test.ts +++ b/exercises/03.literal-types/02.solution.single-source-of-truth/index.test.ts @@ -27,16 +27,16 @@ await test('getRoutePath returns the expected path', () => { assert.strictEqual( solution.getRoutePath('home'), '/', - '🚨 getRoutePath should return the "/" path for home', + '🚨 getRoutePath should return the path stored on routes for home', ) assert.strictEqual( solution.getRoutePath('login'), '/login', - '🚨 getRoutePath should return "/login" for login', + '🚨 getRoutePath should return the path stored on routes for login', ) assert.strictEqual( solution.getRoutePath('settings'), '/settings', - '🚨 getRoutePath should return "/settings" for settings', + '🚨 getRoutePath should return the path stored on routes for settings', ) }) diff --git a/exercises/04.intersection-types/01.problem.combining-types/README.mdx b/exercises/04.intersection-types/01.problem.combining-types/README.mdx index 25c85ad..1b053d0 100644 --- a/exercises/04.intersection-types/01.problem.combining-types/README.mdx +++ b/exercises/04.intersection-types/01.problem.combining-types/README.mdx @@ -7,8 +7,22 @@ blocks. 🐨 Open and: -1. Create base types for common properties -2. Combine them with `&` to create entity types -3. Use the combined types in functions +1. Create base types with these runtime field types: + - `WithId` β€” `id` (`string`) + - `WithTimestamps` β€” `createdAt` and `updatedAt` (`Date`) + - `WithAuthor` β€” `authorId` and `authorName` (`string`) +2. Build entity types by intersecting those bases (and adding entity-specific + fields): + - `User` β€” id + timestamps, plus `name` and `email` (`string`) + - `Post` β€” id + timestamps + author, plus `title` and `content` (`string`) + - `Comment` β€” id + timestamps + author, plus `text` and `postId` (`string`) +3. Create sample `user`, `post`, and `comment` values and export them by name + +## Completion criteria + +- Named exports: `user`, `post`, `comment` +- `user` has string `id` / `name` / `email` and `Date` `createdAt` / `updatedAt` +- `post` / `comment` also include author fields (`authorId`, `authorName`) and + their own content fields (`title`/`content` or `text`/`postId`) πŸ“œ [TypeScript Handbook - Intersection Types](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) diff --git a/exercises/04.intersection-types/01.problem.combining-types/index.ts b/exercises/04.intersection-types/01.problem.combining-types/index.ts index ca41324..e4c6380 100644 --- a/exercises/04.intersection-types/01.problem.combining-types/index.ts +++ b/exercises/04.intersection-types/01.problem.combining-types/index.ts @@ -1,21 +1,22 @@ // Combining Types with Intersection // 🐨 Create base types named `WithId`, `WithTimestamps`, and `WithAuthor`. -// Use these properties: -// - WithId: id -// - WithTimestamps: createdAt, updatedAt -// - WithAuthor: authorId, authorName +// Required fields / runtime types: +// - WithId: id (string) +// - WithTimestamps: createdAt, updatedAt (Date) +// - WithAuthor: authorId, authorName (string) -// 🐨 Create a User type by combining WithId and WithTimestamps -// plus name and email properties +// 🐨 Create a User type by intersecting WithId and WithTimestamps, +// and also include name and email (string) -// 🐨 Create a Post type by combining WithId, WithTimestamps, and WithAuthor -// plus title and content properties +// 🐨 Create a Post type by intersecting WithId, WithTimestamps, and WithAuthor, +// and also include title and content (string) -// 🐨 Create a Comment type by combining WithId, WithTimestamps, and WithAuthor -// plus text and postId properties +// 🐨 Create a Comment type by intersecting WithId, WithTimestamps, and WithAuthor, +// and also include text and postId (string) // 🐨 Create sample `user`, `post`, and `comment` values that match your types. +// πŸ’° Timestamps must be real Date instances at runtime. // 🐨 Export `user`, `post`, and `comment`. Tests import these by name and check // their shapes. // export { user, post, comment } diff --git a/exercises/05.any-vs-unknown/01.problem.escape-hatches/README.mdx b/exercises/05.any-vs-unknown/01.problem.escape-hatches/README.mdx index 700c3a1..b864727 100644 --- a/exercises/05.any-vs-unknown/01.problem.escape-hatches/README.mdx +++ b/exercises/05.any-vs-unknown/01.problem.escape-hatches/README.mdx @@ -7,8 +7,21 @@ how `any` and `unknown` differ. 🐨 Open and: -1. See how `any` allows anything (dangerous!) -2. See how `unknown` requires checks (safe!) -3. Refactor `any` code to use `unknown` +1. Read `dangerousProcess` β€” it uses `any` and can call methods unsafely +2. Implement `safeProcess` so unknown input is handled safely. Required outputs: + - string β†’ uppercased (`hello` β†’ `HELLO`) + - number β†’ two decimal places (`123` β†’ `123.00`, `3.14159` β†’ `3.14`) + - boolean β†’ `true` / `false` + - other values β†’ their usual string conversion (`null` β†’ `null`, `undefined` + β†’ `undefined`, and `{}` β†’ `[object Object]`) +3. Export `safeProcess` by name (you can leave `dangerousProcess` as the contrast + example) + +## Completion criteria + +- Named export: `safeProcess` +- Matches the string / number / boolean outputs above +- Matches the listed outputs for `null`, `undefined`, and plain objects without + throwing πŸ“œ [TypeScript Handbook - Unknown](https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown) diff --git a/exercises/05.any-vs-unknown/01.problem.escape-hatches/index.ts b/exercises/05.any-vs-unknown/01.problem.escape-hatches/index.ts index 144f9e9..15ff4ca 100644 --- a/exercises/05.any-vs-unknown/01.problem.escape-hatches/index.ts +++ b/exercises/05.any-vs-unknown/01.problem.escape-hatches/index.ts @@ -8,8 +8,12 @@ function dangerousProcess(value: any): string { dangerousProcess('example') -// 🐨 Rewrite this function using `unknown` instead of `any`. -// Handle string, number, and at least one other type safely. +// 🐨 Implement `safeProcess` for unknown input +// Required outputs: +// - string β†’ uppercased +// - number β†’ two decimal places +// - boolean β†’ true / false +// - other values β†’ string conversion without throwing // 🐨 Export `safeProcess`. Tests import it by name and check runtime behavior. // export { safeProcess } diff --git a/exercises/05.any-vs-unknown/01.solution.escape-hatches/index.test.ts b/exercises/05.any-vs-unknown/01.solution.escape-hatches/index.test.ts index 0dbdd43..38a1a50 100644 --- a/exercises/05.any-vs-unknown/01.solution.escape-hatches/index.test.ts +++ b/exercises/05.any-vs-unknown/01.solution.escape-hatches/index.test.ts @@ -13,17 +13,17 @@ await test('safeProcess should handle string values', () => { assert.strictEqual( solution.safeProcess('hello'), 'HELLO', - '🚨 safeProcess should uppercase strings - check your type handling for string values', + '🚨 safeProcess should uppercase string inputs', ) assert.strictEqual( solution.safeProcess('test'), 'TEST', - '🚨 safeProcess should uppercase strings - check your type handling for string values', + '🚨 safeProcess should uppercase string inputs', ) assert.strictEqual( solution.safeProcess(''), '', - '🚨 safeProcess should handle empty strings - check your type handling for string values', + '🚨 safeProcess should handle empty strings', ) }) @@ -31,17 +31,17 @@ await test('safeProcess should handle number values', () => { assert.strictEqual( solution.safeProcess(123), '123.00', - '🚨 safeProcess should format numbers to 2 decimal places - check your type handling for number values', + '🚨 safeProcess should format numbers to two decimal places', ) assert.strictEqual( solution.safeProcess(0), '0.00', - '🚨 safeProcess should format zero correctly - check your type handling for number values', + '🚨 safeProcess should format zero to two decimal places', ) assert.strictEqual( solution.safeProcess(3.14159), '3.14', - '🚨 safeProcess should round numbers to 2 decimal places - check your type handling for number values', + '🚨 safeProcess should round numbers to two decimal places', ) }) @@ -49,12 +49,12 @@ await test('safeProcess should handle boolean values', () => { assert.strictEqual( solution.safeProcess(true), 'true', - '🚨 safeProcess should convert booleans to strings - check your type handling for boolean values', + '🚨 safeProcess should turn booleans into lowercase true/false strings', ) assert.strictEqual( solution.safeProcess(false), 'false', - '🚨 safeProcess should convert booleans to strings - check your type handling for boolean values', + '🚨 safeProcess should turn booleans into lowercase true/false strings', ) }) @@ -62,16 +62,16 @@ await test('safeProcess should handle other types', () => { assert.strictEqual( solution.safeProcess(null), 'null', - '🚨 safeProcess should convert null to string - check your type handling for null values', + '🚨 safeProcess should stringify null', ) assert.strictEqual( solution.safeProcess(undefined), 'undefined', - '🚨 safeProcess should convert undefined to string - check your type handling for undefined values', + '🚨 safeProcess should stringify undefined', ) assert.strictEqual( solution.safeProcess({}), '[object Object]', - '🚨 safeProcess should convert objects to string representation - check your type handling for object values', + '🚨 safeProcess should stringify plain objects', ) }) diff --git a/exercises/06.generics/01.problem.generic-functions/README.mdx b/exercises/06.generics/01.problem.generic-functions/README.mdx index bd231c9..723a019 100644 --- a/exercises/06.generics/01.problem.generic-functions/README.mdx +++ b/exercises/06.generics/01.problem.generic-functions/README.mdx @@ -23,8 +23,18 @@ identity(42) // Inferred: Value is number 🐨 Open and: -1. Create a generic `identity` function that returns its input -2. Create a generic `last` function that returns the last array element +1. Create a generic `identity` function that takes a value and returns the same + value (preserving its type) +2. Create a generic `last` function that takes an `Array` and returns + `Item | undefined` (the last element, or `undefined` for an empty array) +3. Export `identity` and `last` by name + +## Completion criteria + +- Named exports: `identity`, `last` +- `identity` returns the same value for strings, numbers, booleans, and `null` +- `last([1, 2, 3])` β†’ `3`; `last(['a', 'b', 'c'])` β†’ `'c'`; `last([])` β†’ + `undefined` πŸ’° Use a type parameter to make the array function work with any type. diff --git a/exercises/06.generics/02.problem.generic-types/README.mdx b/exercises/06.generics/02.problem.generic-types/README.mdx index d42b05c..f0e728a 100644 --- a/exercises/06.generics/02.problem.generic-types/README.mdx +++ b/exercises/06.generics/02.problem.generic-types/README.mdx @@ -41,10 +41,25 @@ type guards to ensure the data is the correct shape. 🐨 Open and: -1. Create a generic `LoadingState` type for async operations -2. Create helper functions `createSuccess` and `createError` -3. See how TypeScript narrows the type based on the discriminated union +1. Create a generic `LoadingState` discriminated union with four statuses: + - idle β€” no payload + - loading β€” no payload + - success β€” carries `data` of type `Data` + - error β€” carries an `error` string +2. Implement `createSuccess` so it returns a success state whose `data` is the + argument you passed in +3. Implement `createError` so it returns an error state whose `error` is the + message string you passed in (keep it generic over `Data` for typing) +4. Export `createSuccess` and `createError` by name -πŸ’° Model idle, loading, success, and error states with a generic discriminated union. +## Completion criteria + +- Named exports: `createSuccess`, `createError` +- Success results have status `success` and preserve the input in `data` +- Error results have status `error` and preserve the message in `error` +- Works with string, number, and object data via generics + +πŸ’° Model idle, loading, success, and error as exclusive variants of one generic +type. πŸ“œ [TypeScript Handbook - Generic Types](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-types) diff --git a/exercises/06.generics/02.problem.generic-types/index.ts b/exercises/06.generics/02.problem.generic-types/index.ts index b5b269e..2ec6292 100644 --- a/exercises/06.generics/02.problem.generic-types/index.ts +++ b/exercises/06.generics/02.problem.generic-types/index.ts @@ -1,20 +1,19 @@ // Generic Types and Interfaces -// 🐨 Create a LoadingState type that represents: -// - { status: 'idle' } -// - { status: 'loading' } -// - { status: 'success', data: Data } -// - { status: 'error', error: string } -// πŸ’° Model all four states with a generic discriminated union +// 🐨 Create a LoadingState discriminated union with four exclusive states: +// - idle (no payload) +// - loading (no payload) +// - success (includes data of type Data) +// - error (includes an error string) -// 🐨 Create a function `createSuccess` that: -// - takes data of type Data -// - returns a LoadingState with status 'success' +// 🐨 Create a function `createSuccess` that takes data and returns a success +// LoadingState carrying that data -// 🐨 Create a function `createError` that: -// - takes an error message string -// - returns a LoadingState with status 'error' +// 🐨 Create a function `createError` that takes an error message and returns an +// error LoadingState carrying that message +// πŸ’° Keep a Data type parameter even though error states have no data field +// type User = { id: number; name: string } // const userState = createSuccess({ id: 1, name: 'Ada' }) // console.log(userState) // const errorState = createError('Failed to load user') diff --git a/exercises/06.generics/03.problem.constraints/README.mdx b/exercises/06.generics/03.problem.constraints/README.mdx index 011b6f4..4b0864f 100644 --- a/exercises/06.generics/03.problem.constraints/README.mdx +++ b/exercises/06.generics/03.problem.constraints/README.mdx @@ -26,9 +26,20 @@ Use `extends` to add constraints. 🐨 Open and: -1. Create a constrained function that requires objects with an `id` property -2. Use `keyof` constraint for type-safe property access (we'll cover type operators in depth in the Advanced TypeScript workshop) -3. Create a function with multiple constrained type parameters +1. Implement `getId` β€” accepts objects that have `id: string` and returns that + `id` +2. Implement `getProperty` β€” takes an object and a key of that object, returns + the value at that key with the correct type +3. Implement `merge` β€” takes two objects and returns one object with properties + from both; when keys overlap, the second argument wins +4. Export `getId`, `getProperty`, and `merge` by name + +## Completion criteria + +- Named exports: `getId`, `getProperty`, `merge` +- `getId({ id: '1', name: 'Alice' })` β†’ `'1'` +- `getProperty(user, 'email')` returns the email string +- `merge({ a: 1, b: 2 }, { b: 3, c: 4 })` β†’ `{ a: 1, b: 3, c: 4 }` This is a small preview of **type operators**. We'll dive deeper into these in diff --git a/exercises/06.generics/03.problem.constraints/index.ts b/exercises/06.generics/03.problem.constraints/index.ts index 7769031..896330f 100644 --- a/exercises/06.generics/03.problem.constraints/index.ts +++ b/exercises/06.generics/03.problem.constraints/index.ts @@ -3,21 +3,22 @@ // 🐨 Create a function `getId` that: // - Takes any object type with an `id: string` property // - Returns the id -// πŸ’° Limit ItemWithId to objects with an id property +// πŸ’° Constrain the type parameter so only objects with `id: string` are allowed // 🐨 Create a function `getProperty` that: -// - Takes an object ObjectType and a key Key (where Key is a key of ObjectType) -// - Returns the value at that key with correct type -// πŸ’° Make sure the return type matches the property type +// - Takes an object and a key that exists on that object +// - Returns the value at that key with the correct type +// πŸ’° Constrain the key type parameter with `keyof` // const user = { id: '1', name: 'Alice', email: 'alice@example.com' } // console.log(getId(user)) // '1' // console.log(getProperty(user, 'email')) // 'alice@example.com' // 🐨 Create a function `merge` that: -// - Takes two objects of types Left and Right (both must be objects) -// - Returns a merged object of type Left & Right -// πŸ’° Both parameters should be objects +// - Takes two objects +// - Returns a new object with properties from both +// - If both have the same key, the second object's value wins +// πŸ’° Constrain both type parameters to object types // const merged = merge({ a: 1 }, { b: 2 }) // { a: 1, b: 2 } // console.log(merged) diff --git a/exercises/07.interfaces/01.problem.object-shapes/README.mdx b/exercises/07.interfaces/01.problem.object-shapes/README.mdx index 8799027..9a9972b 100644 --- a/exercises/07.interfaces/01.problem.object-shapes/README.mdx +++ b/exercises/07.interfaces/01.problem.object-shapes/README.mdx @@ -7,7 +7,25 @@ way to define object shapes in TypeScript. 🐨 Open and: -1. Create a `Product` interface with a union type property -2. Create a function that accepts this interface +1. Create a `Product` interface with: + - `id: string`, `name: string`, `price: number` + - `status: 'active' | 'inactive' | 'discontinued'` + - optional `description?: string` +2. Implement `getProductSummary(product: Product): string` that includes the + name, price, and either the description or a clear no-description fallback + (intended shape: name, price, then description text) +3. Create `product` (no description) and `productWithDesc` (with + `description: 'Has description'`) sample values +4. Export `getProductSummary`, `product`, and `productWithDesc` by name + +## Completion criteria + +- Named exports: `getProductSummary`, `product`, `productWithDesc` +- Both samples use a valid `status` union member +- `product.description` is `undefined`; `productWithDesc.description` is + `'Has description'` +- Summary includes name and price, and uses the description when present + (otherwise a no-description fallback) so the two samples produce different + strings πŸ“œ [TypeScript Handbook - Interfaces](https://www.typescriptlang.org/docs/handbook/2/objects.html) diff --git a/exercises/07.interfaces/01.problem.object-shapes/index.ts b/exercises/07.interfaces/01.problem.object-shapes/index.ts index a60160e..b51fa09 100644 --- a/exercises/07.interfaces/01.problem.object-shapes/index.ts +++ b/exercises/07.interfaces/01.problem.object-shapes/index.ts @@ -7,11 +7,12 @@ // - status: 'active' | 'inactive' | 'discontinued' // - description?: string (optional) -// 🐨 Create a function `getProductSummary` that takes a Product -// and returns a string summary +// 🐨 Create a function `getProductSummary` that takes a Product and returns a +// string including name, price, and either the description or a no-description +// fallback when description is missing. -// 🐨 Create sample `product` and `productWithDesc` values that -// match your interface. +// 🐨 Create sample `product` (no description) and `productWithDesc` +// (`description: 'Has description'`) values that match your interface. // 🐨 Export `getProductSummary`, `product`, and `productWithDesc`. // Tests import these by name and check behavior and shapes. // export { getProductSummary, product, productWithDesc } diff --git a/exercises/07.interfaces/01.solution.object-shapes/index.test.ts b/exercises/07.interfaces/01.solution.object-shapes/index.test.ts index 9d960f1..ac77d9f 100644 --- a/exercises/07.interfaces/01.solution.object-shapes/index.test.ts +++ b/exercises/07.interfaces/01.solution.object-shapes/index.test.ts @@ -27,16 +27,22 @@ await test('getProductSummary should format products correctly', () => { const noDescResult = solution.getProductSummary(solution.product) const withDescResult = solution.getProductSummary(solution.productWithDesc) - // Results should include product name and price assert.ok( - typeof noDescResult === 'string' && noDescResult.length > 0, - '🚨 getProductSummary should return a non-empty string', + noDescResult.includes(solution.product.name) && + noDescResult.includes(String(solution.product.price)), + '🚨 The summary should include the product name and price', + ) + assert.match( + noDescResult, + /no description/i, + '🚨 The summary should clearly indicate when no description is available', ) assert.ok( - typeof withDescResult === 'string' && withDescResult.length > 0, - '🚨 getProductSummary should return a non-empty string', + withDescResult.includes(solution.productWithDesc.name) && + withDescResult.includes(String(solution.productWithDesc.price)) && + withDescResult.includes(solution.productWithDesc.description ?? ''), + '🚨 The summary should include the product name, price, and description', ) - // Results should be different when description is present vs absent assert.notStrictEqual( noDescResult, withDescResult, diff --git a/exercises/07.interfaces/02.problem.extending-interfaces/README.mdx b/exercises/07.interfaces/02.problem.extending-interfaces/README.mdx index aa415be..17d2af0 100644 --- a/exercises/07.interfaces/02.problem.extending-interfaces/README.mdx +++ b/exercises/07.interfaces/02.problem.extending-interfaces/README.mdx @@ -20,8 +20,20 @@ const dog: Dog = { name: 'Rex', breed: 'German Shepherd' } 🐨 Open and: -1. Create a base `Entity` interface with common fields -2. Extend it to create `User` and `Product` interfaces -3. Use multiple inheritance with `extends A, B` +1. Create a `Timestamps` interface with `createdAt: Date` and `updatedAt: Date` +2. Create an `Entity` interface that extends `Timestamps` and adds `id: string` +3. Create `User` and `Product` interfaces that extend `Entity` with their own + fields (`name`/`email` and `name`/`price`) +4. Create an `AuditLog` interface that extends `Timestamps` (not `Entity`) with + `action: string` and `userId: string` +5. Create sample `user`, `product`, `log`, and `entity` values and export them by + name + +## Completion criteria + +- Named exports: `user`, `product`, `log`, `entity` +- `user` / `product` include `id`, timestamps (`Date`), and their own fields +- `log` has timestamps plus `action` / `userId` (no `id` required) +- `entity` has `id` plus `createdAt` / `updatedAt` as `Date`s πŸ“œ [TypeScript Handbook - Extending Types](https://www.typescriptlang.org/docs/handbook/2/objects.html#extending-types) diff --git a/exercises/07.interfaces/02.problem.extending-interfaces/index.ts b/exercises/07.interfaces/02.problem.extending-interfaces/index.ts index 69effbd..5959133 100644 --- a/exercises/07.interfaces/02.problem.extending-interfaces/index.ts +++ b/exercises/07.interfaces/02.problem.extending-interfaces/index.ts @@ -15,12 +15,12 @@ // - name: string // - price: number -// 🐨 Create an AuditLog interface that extends Timestamps with: +// 🐨 Create an AuditLog interface that extends Timestamps (not Entity) with: // - action: string // - userId: string // 🐨 Create sample `user`, `product`, `log`, and `entity` values that match your -// interfaces. +// interfaces. Use `new Date(...)` for timestamp fields. // 🐨 Export `user`, `product`, `log`, and `entity`. Tests import these by name // and check their shapes. // export { user, product, log, entity } diff --git a/exercises/07.interfaces/02.solution.extending-interfaces/index.test.ts b/exercises/07.interfaces/02.solution.extending-interfaces/index.test.ts index 0d7796d..9f06f3a 100644 --- a/exercises/07.interfaces/02.solution.extending-interfaces/index.test.ts +++ b/exercises/07.interfaces/02.solution.extending-interfaces/index.test.ts @@ -87,11 +87,11 @@ await test('AuditLog should have timestamps and action', () => { ) assert.ok( solution.log.createdAt instanceof Date, - '🚨 log.createdAt should be a Date instance - ensure AuditLog extends Entity interface', + '🚨 log.createdAt should be a Date instance - ensure AuditLog extends Timestamps', ) assert.ok( solution.log.updatedAt instanceof Date, - '🚨 log.updatedAt should be a Date instance - ensure AuditLog extends Entity interface', + '🚨 log.updatedAt should be a Date instance - ensure AuditLog extends Timestamps', ) }) diff --git a/exercises/07.interfaces/03.problem.declaration-merging/README.mdx b/exercises/07.interfaces/03.problem.declaration-merging/README.mdx index 7b54e5a..aebf4f0 100644 --- a/exercises/07.interfaces/03.problem.declaration-merging/README.mdx +++ b/exercises/07.interfaces/03.problem.declaration-merging/README.mdx @@ -34,14 +34,14 @@ This is impossible with type aliasesβ€”you'd get a "duplicate identifier" error. `declare global`, you can augment interfaces from other modules. ```ts -// config.ts +// config.ts β€” must be a module (has an import or export) declare global { interface Config { appName: string } } -// theme-config.ts +// theme-config.ts β€” also a module declare global { interface Config { theme: 'light' | 'dark' @@ -68,8 +68,12 @@ declarations into a single interface! **About imports and exports:** We haven't covered modules in detail yet, but here's what you need to know: -- `import './file.ts'` - Imports a file to run its code (side effects) +- `import './file.ts'` - Imports a file to run its side effects / activate merges - `export { name }` - Exports a value so other files can use it +- A `.ts` file is a **module** when it has at least one top-level `import` or + `export`. Augmentation files must be modules so they can be imported; if you + have nothing else to export, an empty export is enough to mark the file as a + module. - `declare global` - Creates or augments global types that can be merged across files For this exercise, you'll import the augment file to activate the declaration @@ -80,11 +84,22 @@ properly later! 🐨 Open and : -1. In `index.ts`, use `declare global` to declare the `Config` interface with - `appName` -2. In `config-augment.ts`, use `declare global` to augment `Config` with `theme` - and `maxConnections` -3. In `index.ts`, import `config-augment.ts` to activate the merge -4. Create a config object with all merged properties (appName, theme, maxConnections) +1. In `index.ts`, use `declare global` to declare `Config` with `appName: string` +2. In `config-augment.ts`, use `declare global` to augment `Config` with + `theme: 'light' | 'dark'` and `maxConnections: number`, and make the file a + module so it can be imported +3. In `index.ts`, side-effect import `./config-augment.ts` to activate the merge +4. Create a `config` object with all merged properties (`appName`, `theme`, + `maxConnections`) +5. Implement `getTheme(config: Config)` that returns `config.theme` +6. Export `config` and `getTheme` by name + +## Completion criteria + +- Named exports: `config`, `getTheme` +- `config.appName` is a string; `config.theme` is `'light'` or `'dark'`; + `config.maxConnections` is a number +- `getTheme(config)` returns that theme value +- `config-augment.ts` is importable as a module πŸ“œ [TypeScript Handbook - Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) diff --git a/exercises/07.interfaces/03.problem.declaration-merging/config-augment.ts b/exercises/07.interfaces/03.problem.declaration-merging/config-augment.ts index 4c87995..1e70a9b 100644 --- a/exercises/07.interfaces/03.problem.declaration-merging/config-augment.ts +++ b/exercises/07.interfaces/03.problem.declaration-merging/config-augment.ts @@ -1,9 +1,11 @@ // 🐨 Use declare global to augment the Config interface from index.ts -// πŸ’° Use: declare global { interface Config { ... } } -// πŸ’° This tells TypeScript to merge this Config interface with the global one +// πŸ’° declare global { interface Config { ... } } // 🐨 Add a `theme` property to Config: // - theme: 'light' | 'dark' // 🐨 Add a `maxConnections` property to Config: // - maxConnections: number + +// 🐨 Make this file a module so `import './config-augment.ts'` is valid +// πŸ’° A file is a module when it has at least one top-level import or export diff --git a/exercises/07.interfaces/03.problem.declaration-merging/index.ts b/exercises/07.interfaces/03.problem.declaration-merging/index.ts index 53a25db..3af6fb0 100644 --- a/exercises/07.interfaces/03.problem.declaration-merging/index.ts +++ b/exercises/07.interfaces/03.problem.declaration-merging/index.ts @@ -5,11 +5,10 @@ // - appName: string // 🐨 Import the config-augment module to activate declaration merging -// πŸ’° You'll need to use: import './config-augment.ts' +// πŸ’° Side-effect import: import './config-augment.ts' // 🐨 Create a `config` object that satisfies the merged Config interface // (it should have appName, theme, and maxConnections) -// πŸ’° The Config interface will be merged with properties from config-augment.ts // 🐨 Create a `getTheme` function that takes a Config and returns its theme diff --git a/exercises/07.interfaces/03.solution.declaration-merging/config-augment.ts b/exercises/07.interfaces/03.solution.declaration-merging/config-augment.ts index 2e9963a..64947de 100644 --- a/exercises/07.interfaces/03.solution.declaration-merging/config-augment.ts +++ b/exercises/07.interfaces/03.solution.declaration-merging/config-augment.ts @@ -6,3 +6,5 @@ declare global { maxConnections: number } } + +export {} diff --git a/exercises/08.enums/01.problem.string-enums/README.mdx b/exercises/08.enums/01.problem.string-enums/README.mdx index c8b952f..9445a32 100644 --- a/exercises/08.enums/01.problem.string-enums/README.mdx +++ b/exercises/08.enums/01.problem.string-enums/README.mdx @@ -16,9 +16,22 @@ enum OrderStatus { 🐨 Open and: -1. Create an `OrderStatus` enum with the statuses above -2. Create an order object that uses the enum -3. Create a function that handles each status +1. Create a string enum `OrderStatus` with the members and values shown above +2. Create an `order` object with `id: string`, `status: OrderStatus`, and + `customerName: string` +3. Implement `getStatusMessage(status: OrderStatus): string` so each enum member + returns a distinct non-empty message (exact wording is up to you) +4. Export `OrderStatus`, `order`, and `getStatusMessage` by name + +## Completion criteria + +- Named exports: `OrderStatus`, `order`, `getStatusMessage` +- `OrderStatus.Pending === 'pending'` (and the other members match their string + values) +- `order.status` is one of the enum values; `id` and `customerName` are non-empty + strings +- `getStatusMessage` returns four different non-empty strings for the four + statuses πŸ“œ [TypeScript Handbook - Enums](https://www.typescriptlang.org/docs/handbook/enums.html) diff --git a/exercises/08.enums/01.problem.string-enums/index.ts b/exercises/08.enums/01.problem.string-enums/index.ts index 09079ba..979b1c5 100644 --- a/exercises/08.enums/01.problem.string-enums/index.ts +++ b/exercises/08.enums/01.problem.string-enums/index.ts @@ -7,15 +7,15 @@ // - Shipped = 'shipped' // - Delivered = 'delivered' -// 🐨 Create an order object with: -// - id: string +// 🐨 Create an `order` object with: +// - id: string (non-empty) // - status: OrderStatus -// - customerName: string +// - customerName: string (non-empty) // console.log(order) -// 🐨 Create a function `getStatusMessage` that takes an OrderStatus -// and returns a user-friendly message +// 🐨 Create a function `getStatusMessage(status: OrderStatus): string` +// Return a distinct non-empty message for each status (exact text is up to you) // console.log(getStatusMessage(order.status)) diff --git a/exercises/08.enums/02.problem.enum-vs-union/README.mdx b/exercises/08.enums/02.problem.enum-vs-union/README.mdx index 8f4aed7..fafd9ca 100644 --- a/exercises/08.enums/02.problem.enum-vs-union/README.mdx +++ b/exercises/08.enums/02.problem.enum-vs-union/README.mdx @@ -2,18 +2,25 @@ -πŸ‘¨β€πŸ’Ό You've already created enums. Now let's migrate them to union types which is -the preferred approach in modern TypeScript. - -The code already has an enum-based logging function. Your task is to create a -union-based version. +πŸ‘¨β€πŸ’Ό You've already practiced string enums. Modern TypeScript often prefers string +literal unions for the same job. The starter includes an enum-based +`logWithEnum` as a reference; add a union-based logging function next to it. 🐨 Open and: -1. Create a new function `logWithUnion` that uses an inline union type instead - of the `LogLevel` enum -2. The union type should be: `'debug' | 'info' | 'warn' | 'error'` -3. Export both functions so we can compare them +1. Leave the existing `LogLevel` enum and `logWithEnum` function in the file as + a reference +2. Add `logWithUnion(level, message)` that accepts the same four levels as a + string literal union (`'debug' | 'info' | 'warn' | 'error'`) instead of the + enum type, and logs in the same `[LEVEL] message` style +3. Export `logWithUnion` by name (you do not need to export the enum version) + +## Completion criteria + +- Named export: `logWithUnion` +- Calling `logWithUnion` with `'debug' | 'info' | 'warn' | 'error'` and a message + string runs without throwing +- The parameter type is a string literal union (not the `LogLevel` enum) πŸ“œ [TypeScript Handbook - Literal Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) diff --git a/exercises/08.enums/02.problem.enum-vs-union/index.ts b/exercises/08.enums/02.problem.enum-vs-union/index.ts index 5ae3c1c..4ff3bd4 100644 --- a/exercises/08.enums/02.problem.enum-vs-union/index.ts +++ b/exercises/08.enums/02.problem.enum-vs-union/index.ts @@ -1,7 +1,7 @@ // Enum vs Union Types -// Start with an enum, migrate to unions +// Enum-based helper kept as a reference; add a union-based function beside it -// Enum approach +// Enum approach (reference β€” leave this in place) enum LogLevel { Debug = 'debug', Info = 'info', @@ -13,10 +13,10 @@ function logWithEnum(level: LogLevel, message: string): void { console.log(`[${level.toUpperCase()}] ${message}`) } -// 🐨 Migrate the function above to use union types instead of the enum -// Replace the LogLevel enum parameter with an inline union type -// The function should accept: 'debug' | 'info' | 'warn' | 'error' -// Name the new function `logWithUnion` +// 🐨 Add `logWithUnion(level, message): void` that uses a string literal union +// instead of the LogLevel enum for the level parameter: +// 'debug' | 'info' | 'warn' | 'error' +// πŸ’° Log in the same style as logWithEnum: `[LEVEL] message` -// 🐨 Export `logWithUnion`. Tests import this by name and check log output. +// 🐨 Export `logWithUnion`. Tests import this by name. // export { logWithUnion } diff --git a/extra/01.practice/README.mdx b/extra/01.practice/README.mdx index d621373..1c1e14b 100644 --- a/extra/01.practice/README.mdx +++ b/extra/01.practice/README.mdx @@ -20,19 +20,29 @@ This exercise covers: πŸ’° Start with the type definitions - they'll help TypeScript guide you as you implement the functions. -πŸ’° Use the test code at the bottom to verify your implementations work correctly. - -## Structure - -The file is organized into sections. Work through them in order: - -1. **Type Aliases & Interfaces** - Build the foundation -2. **Union & Intersection Types** - Combine types safely -3. **Literal Types** - Create single sources of truth -4. **Generics & Constraints** - Write reusable code -5. **Discriminated Unions** - Model complex state -6. **Any vs Unknown** - Handle unknown data safely -7. **Complex Combinations** - Apply everything together +πŸ’° Uncomment the "Test Section" snippets at the bottom of each section to smoke-check your work in the console. + +## What success looks like + +There are no automated tests. You're done with a section when: + +1. **Type Aliases & Interfaces** β€” A `Customer` value type-checks with required + `BaseEntity` fields, nested `Address`, and `isActive` +2. **Union & Intersection Types** β€” `processPayment` narrows on `type` and + returns a distinct descriptive string for card, PayPal, and bank payments +3. **Literal Types** β€” `orderStatuses` uses `as const`; `getStatusLabel('pending')` + returns `'pending'` (and likewise for other keys) using the object as the + source of truth +4. **Generics & Constraints** β€” `getProperty`, `hasProperty`, `mergeObjects`, and + `filterByProperty` type-check at the call sites in the section comments and + produce the expected runtime values +5. **Discriminated Unions** β€” `handleApiResponse` covers loading/success/error + with exhaustive checking; `getFieldValue` returns the field's value for each + `FormField` variant +6. **Any vs Unknown** β€” `safeParseJson` returns an object or `null`; + `isStringArray` is a type guard that returns `true` only for string arrays +7. **Complex Combinations** β€” A `Task` value type-checks; `updateTaskMetadata` + returns a new task with updated `metadata` ## Tips diff --git a/extra/01.practice/index.ts b/extra/01.practice/index.ts index 05c387c..22477fd 100644 --- a/extra/01.practice/index.ts +++ b/extra/01.practice/index.ts @@ -70,9 +70,9 @@ // 🐨 Create a function `processPayment` that: // - Takes a Payment -// - Returns a string describing the payment method +// - Returns a distinct string describing the payment method for each variant // - Uses type narrowing to handle each variant -// πŸ’° Use a switch statement on the 'type' property +// πŸ’° Switch on the 'type' property; exact wording is up to you // Test Section 2: // const cardPayment: Payment = { @@ -98,13 +98,9 @@ // SECTION 3: Literal Types & Single Source of Truth // ============================================================================ -// 🐨 Create a const object `orderStatuses` with: -// - pending: 'pending' -// - processing: 'processing' -// - shipped: 'shipped' -// - delivered: 'delivered' -// - cancelled: 'cancelled' -// πŸ’° Add `as const` to preserve literal types +// 🐨 Create a const object `orderStatuses` with keys/values: +// pending, processing, shipped, delivered, cancelled (each key maps to the same string) +// πŸ’° Add `as const` to the object below so the values stay literal types const orderStatuses = { pending: 'pending', @@ -114,15 +110,14 @@ const orderStatuses = { cancelled: 'cancelled', } -// 🐨 Create a type `OrderStatus` using `keyof typeof orderStatuses` +// 🐨 Create a type `OrderStatus` from the keys of `orderStatuses` -// 🐨 Create a type `OrderStatusValue` using the values from orderStatuses -// πŸ’° Use `typeof orderStatuses[OrderStatus]` +// 🐨 Create a type `OrderStatusValue` from the values of `orderStatuses` // 🐨 Create a function `getStatusLabel` that: // - Takes an OrderStatus // - Returns the corresponding value from orderStatuses -// πŸ’° Use the orderStatuses object as the single source of truth +// πŸ’° Look up the label on orderStatuses (single source of truth) // Test Section 3: // console.log('Pending status:', getStatusLabel('pending')) @@ -134,9 +129,9 @@ const orderStatuses = { // ============================================================================ // 🐨 Create a generic function `getProperty` that: -// - Takes an object ObjectType and a key Key (where Key is a key of ObjectType) +// - Takes an object and a key that exists on that object // - Returns the value at that key with the correct type -// πŸ’° Type: (obj: ObjectType, key: Key) => ObjectType[Key] +// πŸ’° Constrain the key with `keyof` so invalid keys are rejected // 🐨 Create a generic function `hasProperty` that: // - Takes an object and a property name @@ -144,16 +139,14 @@ const orderStatuses = { // πŸ’° Use the `in` operator for type narrowing // 🐨 Create a generic function `mergeObjects` that: -// - Takes two objects Left and Right (both must be objects) -// - Returns a merged object of type Left & Right -// πŸ’° Constrain both parameters to be objects: `extends Record` +// - Takes two objects +// - Returns a merged object combining both (intersection of their types) +// πŸ’° Constrain both parameters to object-like types // 🐨 Create a generic function `filterByProperty` that: -// - Takes an array of items with a property P -// - Takes a property name K (where K is a key of each item) -// - Takes a value V (where V is the type of that property) -// - Returns items where the property matches the value -// πŸ’° Type: (items: Array, key: K, value: T[K]) => Array +// - Takes an array of items, a property key, and a value +// - Returns only items where that property equals the value +// πŸ’° Constrain the key with `keyof` and type the value from the item // Test Section 4: // const testObj = { name: 'Alice', age: 30, active: true } @@ -216,14 +209,14 @@ const orderStatuses = { // 🐨 Create a function `safeParseJson` that: // - Takes an unknown value -// - Returns an object or null +// - Returns a plain object or null (not arrays/null from JSON) // - Uses type guards to safely parse JSON -// πŸ’° Check if the value is a string, parse it, then validate it's an object +// πŸ’° If it's a string, parse it; only return non-null objects // 🐨 Create a function `isStringArray` that: // - Takes an unknown value -// - Returns a type guard: value is Array -// πŸ’° Check if it's an array and all elements are strings +// - Returns a type guard for Array +// πŸ’° Confirm it's an array and every element is a string // Test Section 6: // console.log('Parse JSON:', safeParseJson('{"name": "Alice"}')) @@ -254,9 +247,9 @@ const orderStatuses = { // - assigneeId: string | null // 🐨 Create a generic function `updateTaskMetadata` that: -// - Takes a Task and a key-value pair for metadata -// - Returns a new Task with updated metadata -// πŸ’° Use generics to ensure type safety for the metadata value +// - Takes a Task, a metadata key (string), and a metadata value +// - Returns a new Task with that key set/updated in `metadata` +// πŸ’° Do not mutate the original task; spread a new metadata object // Test Section 7: // const task: Task = { diff --git a/extra/02.epic-task-manager/README.mdx b/extra/02.epic-task-manager/README.mdx index d911789..6cadd4b 100644 --- a/extra/02.epic-task-manager/README.mdx +++ b/extra/02.epic-task-manager/README.mdx @@ -30,22 +30,44 @@ The app will start on `http://localhost:5173` (or another port if 5173 is busy). ### In `utils.ts`: -1. **Type Definitions** - Create discriminated unions, type aliases, and interfaces -2. **formatProjectStatus** - Handle different project status variants -3. **filterByPriority** - Generic function to filter by priority -4. **getUserDisplayName** - Handle null user values -5. **canManageTasks** - Narrow user roles -6. **updateTaskProperty** - Generic function with keyof constraints -7. **getTasksByAssignee** - Handle null and string assignee IDs -8. **createProjectUpdate** - Transition between project statuses +1. **Type Definitions** - `ProjectStatus`, `Priority`, `Task`, `UserRole`, `User` + (replace the `any` placeholders) +2. **formatProjectStatus** - exact strings: + - planning β†’ `Project is in planning phase` + - active β†’ `Project started on {startDate}` + - completed β†’ `Project completed on {endDate} (started {startDate})` +3. **filterByPriority** - return items whose `priority` matches the given value +4. **getUserDisplayName** - `null` β†’ `Unassigned`; otherwise `{name} ({role})` +5. **canManageTasks** - `true` for `'admin'` / `'manager'`, otherwise `false` +6. **updateTaskProperty** - return a new task with one property updated +7. **getTasksByAssignee** - filter by `assigneeId`, including the `null` + (unassigned) case +8. **createProjectUpdate** - takes current status plus `'start' | 'complete'` and + returns the next `ProjectStatus`. On `'start'`, move `planning` β†’ `active` + and set `startDate`. On `'complete'`, move `active` β†’ `completed`, keep + `startDate`, and set `endDate`. Choose ISO date strings inside this helper + (the function signature does not take a date argument). ### In `app.tsx`: -1. Use `filterByPriority` to filter tasks -2. Use `getTasksByAssignee` to filter by assignee -3. Use `formatProjectStatus` to display project status -4. Use `updateTaskProperty` to toggle task completion -5. Use `createProjectUpdate` to transition project statuses +1. Use `filterByPriority` when a priority other than `'all'` is selected +2. Use `getTasksByAssignee` with `selectedUserId` +3. Use `formatProjectStatus` for `statusDisplay` +4. Use `updateTaskProperty` to toggle `completed` in `handleTaskToggle` +5. Call `createProjectUpdate` from the start/complete handlers and store the + returned status (dates are produced by the helper, not passed from the UI) + +## What success looks like + +With `npm run dev` open: + +- Priority and assignee filters change which tasks appear +- Project status text matches `formatProjectStatus` for planning / active / + completed +- Start / Complete project buttons transition status and update the status text +- Toggling a task updates its completed state through `updateTaskProperty` +- Assignee labels use `getUserDisplayName` (including `Unassigned`) +- Manage-task UI respects `canManageTasks` for the current user's role ## Tips diff --git a/extra/02.epic-task-manager/src/app.tsx b/extra/02.epic-task-manager/src/app.tsx index bd5eec3..a6d40a7 100644 --- a/extra/02.epic-task-manager/src/app.tsx +++ b/extra/02.epic-task-manager/src/app.tsx @@ -108,14 +108,14 @@ function App() { } const handleProjectStart = () => { - // 🐨 Use createProjectUpdate to transition from 'planning' to 'active' - // Set startDate to new Date().toISOString() + // 🐨 Call createProjectUpdate(projectStatus, 'start') and store the result + // πŸ’° startDate is set inside createProjectUpdate console.log('Start project') } const handleProjectComplete = () => { - // 🐨 Use createProjectUpdate to transition from 'active' to 'completed' - // Set endDate to new Date().toISOString() + // 🐨 Call createProjectUpdate(projectStatus, 'complete') and store the result + // πŸ’° endDate is set inside createProjectUpdate console.log('Complete project') } diff --git a/extra/02.epic-task-manager/src/utils.ts b/extra/02.epic-task-manager/src/utils.ts index d4232b9..9350a8f 100644 --- a/extra/02.epic-task-manager/src/utils.ts +++ b/extra/02.epic-task-manager/src/utils.ts @@ -58,11 +58,10 @@ export function formatProjectStatus(status: ProjectStatus): string { } // 🐨 Create a generic function `filterByPriority` that: -// - Takes an array of items with a `priority: Priority` property +// - Takes an array of items that each have a `priority: Priority` property // - Takes a Priority value to filter by // - Returns a new array of items matching that priority -// πŸ’° Use a generic constraint to ensure items have a priority property -// πŸ’° Type: (items: Array, priority: Priority) => Array +// πŸ’° Constrain the item type so it must include `priority` export function filterByPriority( items: Array, @@ -99,10 +98,9 @@ export function canManageTasks(role: UserRole): boolean { } // 🐨 Create a generic function `updateTaskProperty` that: -// - Takes a Task, a key K (where K is a key of Task), and a value of type Task[K] -// - Returns a new Task with that property updated -// πŸ’° Use generics with keyof to ensure type safety -// πŸ’° Type: (task: Task, key: K, value: Task[K]) => Task +// - Takes a Task, a key of Task, and a value matching that property's type +// - Returns a new Task with that property updated (do not mutate the original) +// πŸ’° Constrain the key with `keyof Task` export function updateTaskProperty( task: Task, @@ -133,11 +131,10 @@ export function getTasksByAssignee( // 🐨 Create a function `createProjectUpdate` that: // - Takes a ProjectStatus and an update type: 'start' | 'complete' -// - Returns a new ProjectStatus -// - 'start' transitions 'planning' -> 'active' (adds startDate) -// - 'complete' transitions 'active' -> 'completed' (adds endDate) -// πŸ’° Use discriminated unions and exhaustive checking -// πŸ’° Type: (status: ProjectStatus, update: 'start' | 'complete') => ProjectStatus +// - Returns a new ProjectStatus (this helper chooses the date strings) +// - 'start': planning -> active (set startDate) +// - 'complete': active -> completed (keep startDate, set endDate) +// πŸ’° Produce ISO date strings inside this function; the signature has no date arg export function createProjectUpdate( status: ProjectStatus,