TSTypeScript · Lesson 9 of 10

Mini Project: Typed CLI Tool

Let's build a typed command-line tool that queries a public API. We'll use generics, interfaces, async/await, and the Result pattern — all in one cohesive program.

TypeScript
#!/usr/bin/env ts-node
// github-info.ts — query GitHub's public API
// Usage: ts-node github-info.ts <username> [<username2> ...]

interface GHUser {
  login: string
  name: string | null
  bio: string | null
  public_repos: number
  followers: number
  following: number
  location: string | null
  blog: string
  created_at: string
}

type Result<T> =
  | { ok: true;  value: T }
  | { ok: false; error: string }

async function fetchGH<T>(path: string): Promise<Result<T>> {
  try {
    const res = await fetch(`https://api.github.com${path}`, {
      headers: { "Accept": "application/vnd.github.v3+json" }
    })
    if (res.status === 404) return { ok: false, error: "Not found" }
    if (!res.ok)            return { ok: false, error: `HTTP ${res.status}` }
    return { ok: true, value: await res.json() as T }
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e.message : "Network error" }
  }
}

function formatUser(u: GHUser): string {
  const lines: string[] = [
    `── @${u.login} ${u.name ? `(${u.name})` : ""}`,
    u.bio      ? `   ${u.bio}`         : "",
    u.location ? `   📍 ${u.location}` : "",
    u.blog     ? `   🔗 ${u.blog}`     : "",
    `   repos: ${u.public_repos}  followers: ${u.followers}`,
  ]
  return lines.filter(Boolean).join("\n")
}

async function main(): Promise<void> {
  const usernames = process.argv.slice(2)

  if (usernames.length === 0) {
    console.log("Usage: ts-node github-info.ts <username> [username2] ...")
    process.exit(1)
  }

  const results = await Promise.all(
    usernames.map(async (username): Promise<[string, Result<GHUser>]> => [
      username,
      await fetchGH<GHUser>(`/users/${username}`),
    ])
  )

  for (const [username, result] of results) {
    if (result.ok) {
      console.log(formatUser(result.value))
    } else {
      console.error(`✗ ${username}: ${result.error}`)
    }
    console.log()
  }
}

main().catch(err => {
  console.error(err)
  process.exit(1)
})
Bash
ts-node github-info.ts torvalds gvanrossum Rich-Harris
◆ Note
Next steps: add a `tsconfig.json` with `strict: true` (enables every strict check), add `--repos` flag to list repositories, or try running this with `npx tsx` (a faster alternative to ts-node).