Function Types and Return Types
Content
Work through these exercises independently. Each one covers a different part of the curriculum — if you get stuck, the note at the top of each exercise tells you which page to review.
Create a dedicated folder for your work and initialise it as an ES module project:
mkdir typescript-practice
cd typescript-practice
npm init -y
npm pkg set type=module
The type=module setting tells Node.js to treat every .js and .ts file in the folder as an ES module, which is required for top-level await in Exercise 7.
Save each exercise file to your typescript-practice folder, then run it directly:
node types.ts
node shapes.ts
# etc.
Node.js v22.18+ and v23.6+ support TypeScript files natively — no build step needed.
<aside> 💡
Review the Type System and Type Inference sections if you get stuck.
</aside>
Save as: types.ts
You are building a music playlist app. Your job is to add type annotations and write a few typed functions.
title, artist, and year variables with explicit types.Song interface with title: string, artist: string, and duration (in seconds) as a number.currentSong of type Song and assign a value to it.playlist variable typed as Song[] containing at least three songs.formatDuration(seconds: number): string that returns a duration string in "m:ss" format (e.g., 214 → "3:34"). Pad the seconds with a leading zero if needed.getInfo(input: string | Song): string. When input is a string, return it as-is. When it is a Song, return "${song.title} by ${song.artist}". Use a type guard to narrow the union.// Save as: types.ts
// TODO 1: annotate these variables
let title = "Bohemian Rhapsody";
let artist = "Queen";
let year = 1975;
// TODO 2: define the Song interface
// interface Song { ... }
// TODO 3: create currentSong
// const currentSong: Song = { ... }
// TODO 4: create playlist
// const playlist: Song[] = [ ... ]
// TODO 5: implement formatDuration
function formatDuration(seconds: number): string {
// hint: Math.floor(seconds / 60) gives minutes
// hint: String(n).padStart(2, "0") pads with a leading zero
return "";
}
// TODO 6: implement getInfo
function getInfo(input: string | Song): string {
return "";
}
// TODO 7: call and log
console.log(formatDuration(214)); // "3:34"
console.log(getInfo("My playlist"));
// console.log(getInfo(currentSong));
<aside> 💡
Review the Interfaces vs Types section if you get stuck.
</aside>
Save as: shapes.ts
You are modelling data for a simple online shop.
Product interface with name: string, price: number, category: string, and an optional inStock?: boolean.CartItem type alias that combines a Product with a quantity: number field. Use an intersection (&) or inline extension — your choice.formatLineItem(item: CartItem): string that returns a string like "2x Widget — $19.98" (quantity × price, formatted to two decimal places).Product objects and a cart array of CartItem values using those products.cart and log each formatted line item.// Save as: shapes.ts
// TODO 1: define Product interface
// TODO 2: define CartItem type alias
// TODO 3: implement formatLineItem
function formatLineItem(item: CartItem): string {
return "";
}
// TODO 4: create products and cart
// const widget: Product = { ... }
// const gadget: Product = { ... }
// const cart: CartItem[] = [ ... ]
// TODO 5: log each line item
// cart.map(formatLineItem).forEach(line => console.log(line));
<aside> 💡
Review the Function Types section if you get stuck.
</aside>
Save as: transforms.ts
You are building a small data-pipeline helper.
Transformer<T> that represents a function taking a T and returning a T.pipe<T>(value: T, ...fns: Transformer<T>[]): T that applies each function in fns to value in order and returns the final result.number transformers:
double: multiplies by 2addTen: adds 10clamp0to100: clamps the value between 0 and 100pipe by calling pipe(45, double, addTen, clamp0to100) and logging the result.string transformer (e.g., trim, uppercase) and demonstrate pipe with strings too.// Save as: transforms.ts
// TODO 1: define Transformer<T>
// type Transformer<T> = ...
// TODO 2: implement pipe
function pipe<T>(value: T, ...fns: Transformer<T>[]): T {
return value; // replace this
}
// TODO 3: create number transformers
// const double: Transformer<number> = ...
// const addTen: Transformer<number> = ...
// const clamp0to100: Transformer<number> = ...
// TODO 4: demonstrate with numbers
// console.log(pipe(45, double, addTen, clamp0to100)); // 100
// TODO 5: create and demonstrate string transformers
<aside> 💡
Review the Generics section if you get stuck.
</aside>
Save as: generics-practice.ts
You are writing a small utility library for working with arrays.
head<T>(arr: T[]): T | undefined — returns the first element, or undefined for an empty array.tail<T>(arr: T[]): T[] — returns all elements except the first (empty array if fewer than two elements).zip<A, B>(a: A[], b: B[]): [A, B][] — pairs up elements at matching indices. Stop at the shorter array's length.groupBy<T>(arr: T[], key: keyof T): Record<string, T[]> — groups the array into an object keyed by the value of key on each element (convert the value to a string). Add a constraint so T must be an object.User interface with id: number, name: string, and role: "admin" | "member". Create a users array of at least four users and demonstrate all four utility functions with it.// Save as: generics-practice.ts
// TODO 1: implement head
function head<T>(arr: T[]): T | undefined {
return undefined; // replace this
}
// TODO 2: implement tail
function tail<T>(arr: T[]): T[] {
return []; // replace this
}
// TODO 3: implement zip
function zip<A, B>(a: A[], b: B[]): [A, B][] {
return []; // replace this
}
// TODO 4: implement groupBy
function groupBy<T extends object>(arr: T[], key: keyof T): Record<string, T[]> {
return {}; // replace this
}
// TODO 5: define User and demonstrate
interface User {
id: number;
name: string;
role: "admin" | "member";
}
const users: User[] = [
// add at least 4 users, mix of roles
];
// console.log(head(users));
// console.log(tail(users));
// console.log(zip(users, ["a", "b", "c"]));
// console.log(groupBy(users, "role"));
<aside> 💡
</aside>
Save as: utility-practice.ts
You are modelling a user profile system. Start from a complete interface and derive narrower types using built-in utility types.
UserProfile interface with: id: number, username: string, email: string, bio: string, avatarUrl: string, createdAt: string.Omit to create a CreateUserPayload type — everything except id and createdAt (the server sets those).Partial to create an UpdateUserPayload type — all fields are optional because the client only sends what changed.Pick to create a PublicProfile type — only username, bio, and avatarUrl (safe to display publicly).Record to create a UserStore type — an object keyed by user id (as a string) mapping to full UserProfile values.// Save as: utility-practice.ts
// TODO 1: define UserProfile
interface UserProfile {
id: number;
username: string;
email: string;
bio: string;
avatarUrl: string;
createdAt: string;
}
// TODO 2: CreateUserPayload using Omit
// type CreateUserPayload = ...
// TODO 3: UpdateUserPayload using Partial
// type UpdateUserPayload = ...
// TODO 4: PublicProfile using Pick
// type PublicProfile = ...
// TODO 5: UserStore using Record
// type UserStore = ...
// TODO 6: implement the four functions
function createUser(payload: CreateUserPayload): UserProfile {
return { id: 1, createdAt: new Date().toISOString(), ...payload };
}
function updateUser(profile: UserProfile, changes: UpdateUserPayload): UserProfile {
return profile; // replace this
}
function toPublicProfile(profile: UserProfile): PublicProfile {
return profile; // replace this — hint: return only the three public fields
}
function addToStore(store: UserStore, profile: UserProfile): UserStore {
return store; // replace this
}
// Demonstrate each function
const newUser = createUser({
username: "alice",
email: "[email protected]",
bio: "Learning TypeScript",
avatarUrl: "<https://example.com/alice.png>",
});
console.log(newUser);
// console.log(updateUser(newUser, { bio: "TypeScript enthusiast" }));
// console.log(toPublicProfile(newUser));
// console.log(addToStore({}, newUser));
[!NOTE] Review pages 06 (Unions and Intersections) and 10 (Type Guards) if you get stuck.
<aside> 💡
</aside>
Save as: state-machine.ts
You are modelling the state of a file download.
DownloadState discriminated union with four variants, each using a kind field as the discriminant: