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,