155: TypeScript - Functions and Object Design
Learning outcomes
- type function inputs, outputs, callbacks, and async results;
- choose interfaces and type aliases by intent;
- preserve readonly boundaries and avoid accidental mutation.
Study
Function signatures are contracts between modules. Type the result of a transformation and the callback supplied to it. Use readonly where a function should observe rather than modify data. Optional properties mean absence is valid; code still needs to decide how absence behaves.
Practice
Type a task service with createTask, updateTask, listTasks, and deleteTask. Use a command type that permits only editable fields. Add a fake repository and tests proving the service never accepts a client-supplied owner ID.
Boundary design
Do not reuse one Task type for every direction:
type Task = { id: string; ownerId: string; title: string; completed: boolean }
type CreateTask = { title: string }
type UpdateTask = { title?: string; completed?: boolean }
type TaskRepository = {
create(ownerId: string, input: CreateTask): Promise<Task>
update(ownerId: string, id: string, input: UpdateTask): Promise<Task | null>
}
The service supplies ownerId; the client cannot request a different owner. readonly is useful for values that should not be mutated by a formatter, but it is not a security control and does not deep-freeze runtime objects.
Tests
Use a fake repository that records arguments. Assert that create('user-1', { title: 'x' }) receives no body-owned identity field, that a missing update returns a defined not-found result, and that repository rejection becomes a safe service error.
Checkpoint
Show how Promise<Result<Task>> differs from a function that throws. Explain why a broad Record<string, unknown> is not a finished domain model.
