每道题都能直接在上面的编辑器里运行。「参考」是给卡住时看的,建议自己先写一遍再对照。本站不做类型检查,所以请重点关注编译能不能通过和运行结果对不对。
1. 用 interface 描述成绩并统计
给一组成绩,定义 Score 接口,输出平均分(保留一位小数)、最高分的科目、以及所有及格(≥60)的科目。
interface Score {
subject: string;
value: number;
}
const scores: Score[] = [
{ subject: "语文", value: 88 },
{ subject: "数学", value: 95 },
{ subject: "英语", value: 59 },
{ subject: "物理", value: 72 },
];
// 参考
const avg = scores.reduce((sum, s) => sum + s.value, 0) / scores.length;
const best = scores.reduce((a, b) => (b.value > a.value ? b : a));
const passed = scores.filter((s) => s.value >= 60).map((s) => s.subject);
console.log("平均分:", avg.toFixed(1));
console.log("最高分:", best.subject, best.value);
console.log("及格科目:", passed.join("、"));
console.log("不及格:", scores.filter((s) => s.value < 60).map((s) => `${s.subject}(${s.value})`).join("、") || "无");
要注意:reduce 不给初始值时,空数组会抛 TypeError。所以 reduce((sum, s) => ..., 0) 的 0 不能省。
2. 联合类型与类型收窄
写一个 format(value: string | number | boolean | null):字符串去空格后原样输出,数字保留两位小数,布尔输出「是 / 否」,null 输出「(空)」。
type Input = string | number | boolean | null;
// 参考
function format(value: Input): string {
if (value === null) return "(空)";
if (typeof value === "string") return value.trim();
if (typeof value === "number") return value.toFixed(2);
return value ? "是" : "否"; // 到这里只可能是 boolean
}
const cases: Input[] = [" hello ", 3.14159, true, false, null, 0, ""];
for (const c of cases) {
console.log(JSON.stringify(c), "→", format(c));
}
要注意:收窄的顺序很重要。if (value === null) 要放在最前面,否则后面每次都要多判一次。typeof value === "number" 之后,TypeScript 就知道剩下的分支里 value 不可能是数字了。
3. 泛型函数三题
实现三个泛型函数:firstLast<T>(arr: T[]) 返回首尾元素组成的元组;countBy<T>(arr: T[], key: (item: T) => string) 按 key 计数;unique<T>(arr: T[]) 去重。
// 参考
function firstLast<T>(arr: T[]): [T | undefined, T | undefined] {
return [arr[0], arr[arr.length - 1]];
}
function countBy<T>(arr: T[], key: (item: T) => string): Record<string, number> {
const out: Record<string, number> = {};
for (const item of arr) {
const k = key(item);
out[k] = (out[k] ?? 0) + 1;
}
return out;
}
function unique<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
interface User {
name: string;
city: string;
}
const users: User[] = [
{ name: "小明", city: "北京" },
{ name: "小红", city: "上海" },
{ name: "小刚", city: "北京" },
{ name: "小美", city: "广州" },
];
console.log(firstLast([1, 2, 3, 4]), firstLast<string>([]));
console.log(countBy(users, (u) => u.city));
console.log(countBy(users, (u) => u.name));
console.log(unique([1, 2, 2, 3, 3, 3]), unique(["a", "b", "a"]));
要注意:(item: T) => string 是「函数类型」的写法——参数叫 item、类型是 T,返回 string。Record<string, number> 表示「键是字符串、值是数字的对象」。
4. 用 enum 做一个订单状态机
订单状态有 待付款 → 已付款 → 已发货 → 已完成,另外任何状态都能进入 已取消。写一个 next() 函数,只在流转合法时返回下一个状态,否则返回 null。
enum Order {
待付款 = "待付款",
已付款 = "已付款",
已发货 = "已发货",
已完成 = "已完成",
已取消 = "已取消",
}
// 参考:用 Record 描述"每个状态能到哪些状态"
const FLOW: Record<Order, Order[]> = {
[Order.待付款]: [Order.已付款, Order.已取消],
[Order.已付款]: [Order.已发货, Order.已取消],
[Order.已发货]: [Order.已完成],
[Order.已完成]: [],
[Order.已取消]: [],
};
function can(from: Order, to: Order): boolean {
return FLOW[from].includes(to);
}
const path: Order[] = [Order.待付款, Order.已付款, Order.已发货, Order.已完成];
for (let i = 0; i < path.length - 1; i++) {
console.log(`${path[i]} → ${path[i + 1]}:`, can(path[i], path[i + 1]) ? "允许" : "禁止");
}
console.log(`已完成 → 已取消:`, can(Order.已完成, Order.已取消) ? "允许" : "禁止");
console.log("所有状态:", Object.values(FLOW).length, "个");
console.log(Object.keys(Order));
要注意:[Order.待付款]: [...] 里的方括号是「计算属性名」,不是数组。字符串枚举没有反向映射,所以 Object.keys(Order) 得到的是键名数组。
5. 抽象类:图形面积与周长
定义抽象类 Shape,要求子类实现 area() 和 perimeter(),父类提供一个 report() 方法打印两者。再实现 Rect、Circle、Triangle。
abstract class Shape {
constructor(public name: string) {}
abstract area(): number;
abstract perimeter(): number;
report(): void {
console.log(`${this.name}:面积 ${this.area().toFixed(2)},周长 ${this.perimeter().toFixed(2)}`);
}
}
// 参考
class Rect extends Shape {
constructor(private w: number, private h: number) {
super("矩形");
}
area(): number { return this.w * this.h; }
perimeter(): number { return (this.w + this.h) * 2; }
}
class Circle extends Shape {
constructor(private r: number) {
super("圆形");
}
area(): number { return Math.PI * this.r ** 2; }
perimeter(): number { return 2 * Math.PI * this.r; }
}
class Triangle extends Shape {
constructor(private a: number, private b: number, private c: number) {
super("三角形");
}
area(): number { // 海伦公式
const p = this.perimeter() / 2;
return Math.sqrt(p * (p - this.a) * (p - this.b) * (p - this.c));
}
perimeter(): number { return this.a + this.b + this.c; }
}
const shapes: Shape[] = [new Rect(3, 4), new Circle(1), new Triangle(3, 4, 5)];
for (const s of shapes) s.report();
console.log("总面积:", shapes.reduce((sum, s) => sum + s.area(), 0).toFixed(2));
console.log("按面积排序:", shapes.map((s) => s.name).join(" < ") === "" ? "" :
[...shapes].sort((a, b) => a.area() - b.area()).map((s) => `${s.name}(${s.area().toFixed(1)})`).join(" < "));
要注意:子类的构造函数里必须先调用 super(...),才能用 this。abstract 在运行时会被擦掉,所以别指望它真的阻止 new Shape()。
6. 从 stdin 读入数据并解析成对象数组
「输入 (stdin)」面板里填四行:第一行是人数,后面每行是 姓名,年龄,城市。
interface Person {
name: string;
age: number;
city: string;
}
// 参考
function parseLine(line: string): Person | null {
const parts = line.split(",").map((s) => s.trim());
if (parts.length !== 3) return null;
const age = Number(parts[1]);
if (Number.isNaN(age)) return null;
return { name: parts[0], age, city: parts[2] };
}
const countLine = input();
const count = countLine === null ? 0 : Number(countLine);
console.log("声明的人数:", count);
const people: Person[] = [];
const bad: string[] = [];
for (let i = 0; i < count; i++) {
const line = input();
if (line === null) { console.log("输入不够,只读到", people.length, "条"); break; }
const p = parseLine(line);
if (p) people.push(p);
else bad.push(line);
}
console.log("解析成功:", people.length, "条");
console.log(people);
if (bad.length) console.log("格式错误:", bad);
// 按城市分组统计
const byCity: Record<string, number> = {};
for (const p of people) byCity[p.city] = (byCity[p.city] ?? 0) + 1;
console.log("城市分布:", byCity);
console.log("平均年龄:", (people.reduce((s, p) => s + p.age, 0) / (people.length || 1)).toFixed(1));
要注意:input() 读完所有行后返回 null,所以每次都要判断。Number("") 是 0 而不是 NaN,空行要单独处理。
7. 用 keyof + Record 写一个安全的配置读取器
要求:get(key) 只能传配置对象里已有的键,取不到时返回默认值,并且能列出所有键。
const CONFIG = {
site: "run.ban.show",
port: 443,
https: true,
timeoutMs: 15000,
} as const;
type ConfigKey = keyof typeof CONFIG;
// 参考
class ConfigReader {
constructor(private data: typeof CONFIG) {}
get<K extends ConfigKey>(key: K): (typeof CONFIG)[K] {
return this.data[key];
}
getOr<K extends ConfigKey>(key: K, fallback: (typeof CONFIG)[K]): (typeof CONFIG)[K] {
const v = this.data[key];
return v === undefined ? fallback : v;
}
keys(): ConfigKey[] {
return Object.keys(this.data) as ConfigKey[];
}
dump(): string {
return this.keys().map((k) => `${k}=${this.data[k]}`).join(", ");
}
}
const cfg = new ConfigReader(CONFIG);
console.log(cfg.get("site"), cfg.get("port"));
console.log(cfg.get("https"), cfg.get("timeoutMs"));
console.log("所有键:", cfg.keys());
console.log(cfg.dump());
// Partial 的用法:只传需要覆盖的部分
const overrides: Partial<typeof CONFIG> = { port: 8080, https: false };
const merged = { ...CONFIG, ...overrides };
console.log("合并后:", merged);
console.log("键的数量:", Object.keys(merged).length);
要注意:Object.keys() 返回的是 string[],想当成 ConfigKey[] 用必须写 as ConfigKey[]。Partial<T> 把 T 的每个属性都变成可选。
8. 泛型 + async:带重试的模拟请求
写一个 withRetry<T>(fn: () => Promise<T>, times: number):失败就重试,最多 times 次,全失败则抛出最后一个错误。
// 参考
async function withRetry<T>(fn: () => Promise<T>, times: number, label = "任务"): Promise<T> {
let lastError: unknown = null;
for (let i = 1; i <= times; i++) {
try {
const value = await fn();
if (i > 1) console.log(`${label} 第 ${i} 次成功`);
return value;
} catch (e) {
lastError = e;
console.log(`${label} 第 ${i} 次失败:`, e instanceof Error ? e.message : String(e));
}
}
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
interface User {
id: number;
name: string;
}
// 前两次失败、第三次成功的接口
let attempt = 0;
const flaky = (): Promise<User> =>
new Promise((resolve, reject) => {
attempt++;
setTimeout(() => {
if (attempt < 3) reject(new Error(`第 ${attempt} 次网络错误`));
else resolve({ id: 1, name: "小明" });
}, 30);
});
const user = await withRetry(flaky, 5, "获取用户");
console.log("拿到:", user);
// 永远失败的情况:要接住异常
try {
await withRetry(() => Promise.reject(new Error("服务不可用")), 2, "获取订单");
} catch (e) {
console.log("最终失败:", e instanceof Error ? e.message : e);
}
// Promise.allSettled 的结果类型是 PromiseSettledResult<T>[]
const results = await Promise.allSettled([Promise.resolve(1), Promise.reject(new Error("x"))]);
for (const r of results) {
console.log(r.status === "fulfilled" ? `成功:${r.value}` : `失败:${r.reason.message}`);
}
要注意:catch (e) 里的 e 类型是 unknown,不能直接 e.message,要先 e instanceof Error 判断。这是 TypeScript 4.4 之后的规则。
9. 用 satisfies 定义一张路由表
要求:每个路由有 path、title,可选 login;用 satisfies 保证结构正确,同时保留精确的字面量类型,最后打印所有需要登录的路由。
interface Route {
path: string;
title: string;
login?: boolean;
}
// 参考
const ROUTES = {
home: { path: "/", title: "首页" },
python: { path: "/python/", title: "在线运行 Python", login: false },
sql: { path: "/sql/", title: "在线运行 SQL" },
admin: { path: "/admin/", title: "管理后台", login: true },
} satisfies Record<string, Route>;
type RouteKey = keyof typeof ROUTES;
function urlOf(key: RouteKey): string {
return ROUTES[key].path;
}
function needLogin(key: RouteKey): boolean {
return ROUTES[key].login ?? false; // 可选属性要用 ?? 兜底
}
const keys = Object.keys(ROUTES) as RouteKey[];
console.log("全部路由:");
for (const k of keys) {
console.log(` ${k.padEnd(8)} ${urlOf(k).padEnd(10)} ${ROUTES[k].title}${needLogin(k) ? "(需登录)" : ""}`);
}
console.log("需要登录的:", keys.filter(needLogin));
console.log("标题总字数:", keys.reduce((s, k) => s + ROUTES[k].title.length, 0));
要注意:satisfies 和 : Record<string, Route> 的区别是——前者检查完之后 ROUTES.home 的类型仍然是精确的 { path: "/", title: "首页" },后者会被拓宽成 Route,就取不到具体的键了。
10. 类型守卫:把不可信的 JSON 变成 User[]
从一段 JSON 字符串解析出用户数组,但要逐条校验字段类型,非法的丢掉并记录原因。
interface User {
id: number;
name: string;
age: number;
email?: string;
}
// 参考:类型谓词函数,返回 v is User
function isUser(v: unknown): v is User {
if (typeof v !== "object" || v === null) return false;
const o = v as Record<string, unknown>;
return typeof o.id === "number"
&& typeof o.name === "string"
&& typeof o.age === "number"
&& (o.email === undefined || typeof o.email === "string");
}
const raw = `[
{"id": 1, "name": "小明", "age": 18, "email": "xm@example.com"},
{"id": 2, "name": "小红", "age": 20},
{"id": "3", "name": "小刚", "age": 17},
{"id": 4, "name": "小美", "age": "十六"},
{"id": 5, "name": "小强", "age": 19, "email": null},
"我不是对象"
]`;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (e) {
console.log("JSON 解析失败:", e instanceof Error ? e.message : e);
}
const users: User[] = [];
const rejected: string[] = [];
if (Array.isArray(parsed)) {
for (const [i, item] of parsed.entries()) {
if (isUser(item)) users.push(item);
else rejected.push(`第 ${i + 1} 条:${JSON.stringify(item)}`);
}
}
console.log("合法用户:", users.length, "条");
console.log(users);
console.log("被丢弃:");
for (const r of rejected) console.log(" ", r);
console.log("平均年龄:", (users.reduce((s, u) => s + u.age, 0) / (users.length || 1)).toFixed(1));
要注意:JSON.parse 返回的是 any,直接当 User[] 用是非常常见的 bug 来源。运行时判断类型只能靠 typeof / Array.isArray / instanceof,v is User 只是把判断结果告诉编译器。第 5 条 email: null 被丢弃,是因为校验要求 email 要么不存在要么是字符串。