下面每一段都可以直接复制到上面的编辑器里运行。本站只做「编译 + 执行」,不做类型检查,所以类型写错不会报错,重点是让你熟悉语法。
变量与类型注解
// 类型注解写在冒号后面:变量名: 类型
let name: string = "小明";
let age: number = 18;
let passed: boolean = true;
let nothing: null = null;
let notSet: undefined = undefined;
// 不写类型也行,编译器会自动推断(推荐这样写)
let city = "北京"; // 推断成 string
let score = 95.5; // 推断成 number
// any 表示"不检查类型",unknown 表示"未知,用之前必须先判断"
let anything: any = "文本";
anything = 123; // any 可以随便改
const list: string[] = ["语文", "数学", "英语"];
const mixed: (string | number)[] = ["分数", 95, "排名", 3];
console.log(name, age, passed, city, score);
console.log(list, mixed);
console.log(typeof name, typeof age, Array.isArray(list));
要注意:类型注解只是给人和工具看的,编译成 JavaScript 后全部被删掉。运行时 typeof 得到的仍然是 JavaScript 的类型。
interface 与 type
interface Student {
id: number;
name: string;
age: number;
email?: string; // 问号表示可选,可以不传
readonly sid: string; // 只读,赋值后不能再改
}
// type 也能描述对象结构,还能写联合类型
type Score = { subject: string; value: number };
type ID = string | number;
const s: Student = { id: 1, name: "小明", age: 18, sid: "S001" };
const sc: Score = { subject: "数学", value: 95 };
const uid: ID = "S001";
// 接口可以继承
interface Monitor extends Student {
duty: string;
}
const m: Monitor = { id: 2, name: "小红", age: 20, sid: "S002", duty: "班长" };
console.log(s);
console.log(sc, uid);
console.log(m.name, m.duty);
console.log("email 存在吗:", "email" in s);
interface 和 type 怎么选:描述对象结构用 interface(能继承、能重复声明合并),需要联合类型、元组、映射类型时用 type。初学阶段两者大部分场景可以互换。
联合、交叉与字面量类型
// 联合类型:可以是几种类型之一
type Grade = "A" | "B" | "C" | "D"; // 字面量联合,只能填这几个值
type StringOrNumber = string | number;
// 交叉类型:同时满足多个类型
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
const p: Person = { name: "小刚", age: 17 };
function format(value: string | number): string {
// 用 typeof 收窄类型,收窄之后就能用对应的专属方法
if (typeof value === "string") return value.trim().toUpperCase();
return value.toFixed(2);
}
function printId(id: string | number) {
console.log("id 是", id, "类型是", typeof id);
}
const g: Grade = "B";
console.log(format(" hello "), format(3.14159));
printId(1001);
printId("S-1002");
console.log(p, g);
要注意:联合类型不能直接调用某一种类型独有的方法。value.toUpperCase() 在 string | number 上是不允许的,必须先用 typeof / in / instanceof 判断(叫「类型收窄」)。本站不做类型检查,所以写了也能跑,但真项目里会报错。
函数与泛型
// 参数和返回值都可以标类型
function add(a: number, b: number): number {
return a + b;
}
// 可选参数用 ?,默认值直接写 =
function greet(name: string, greeting: string = "你好", punct?: string): string {
return `${greeting},${name}${punct ?? ""}`;
}
// 箭头函数
const multiply = (a: number, b: number): number => a * b;
// 泛型:<T> 是类型占位符,调用时自动推断
function first<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
// 泛型约束:T 必须有 length 属性
function logLength<T extends { length: number }>(value: T): T {
console.log("长度是", value.length);
return value;
}
// 函数类型可以单独描述
type Comparator = (a: number, b: number) => number;
const desc: Comparator = (a, b) => b - a;
console.log(add(3, 5));
console.log(greet("小明"), greet("小红", "早上好", "!"));
console.log(multiply(4, 6));
console.log(first([10, 20, 30]), first<string>(["甲", "乙"]), first([]));
console.log(pair("语文", 95));
logLength("hello world");
logLength([1, 2, 3]);
console.log([3, 1, 2].sort(desc));
要注意:<T extends { length: number }> 里的 extends 是「约束」,不是继承。first<string>([...]) 这种显式传类型参数的写法,只在推断不出来时才需要。
class 与访问修饰符
class Animal {
// 构造函数参数前面加修饰符,会自动变成同名属性并赋值(叫"参数属性")
constructor(public name: string, private sound: string, readonly legs: number = 4) {}
speak(): string {
return `${this.name} 说:${this.sound}`;
}
// getter / setter
get info(): string {
return `${this.name}(${this.legs} 条腿)`;
}
}
// 抽象类不能直接 new,只能被继承;抽象方法必须被子类实现
abstract class Shape {
abstract area(): number;
describe(): string {
return `面积是 ${this.area().toFixed(2)}`;
}
}
class Rect extends Shape {
constructor(private w: number, private h: number) {
super();
}
area(): number {
return this.w * this.h;
}
}
class Circle extends Shape {
constructor(private r: number) {
super();
}
area(): number {
return Math.PI * this.r * this.r;
}
}
// 接口也可以被类实现
interface Printable {
print(): void;
}
class Doc implements Printable {
print(): void {
console.log("打印文档");
}
}
const a = new Animal("猫", "喵");
console.log(a.speak(), a.info, a.name);
const shapes: Shape[] = [new Rect(3, 4), new Circle(1)];
for (const s of shapes) {
console.log(s.constructor.name, s.describe());
}
new Doc().print();
console.log("是 Shape 的实例吗:", shapes[0] instanceof Shape);
要注意:private 只在编译期有效,编译成 JavaScript 后就消失了,运行时依然能用 a.sound 读到值。想要运行时真正私有,得用 JavaScript 的 #sound 语法。
enum 枚举
// 数字枚举:默认从 0 开始自增
enum Direction {
上,
下,
左,
右,
}
// 指定起始值
enum Level {
初级 = 1,
中级 = 2,
高级 = 3,
}
// 字符串枚举:更常用于状态、类型码
enum Status {
Pending = "待处理",
Done = "已完成",
Failed = "失败",
}
// 异构与计算值
enum Calc {
A = 1,
B = A * 3,
C, // 紧跟在数字后面,自动 +1
}
const d: Direction = Direction.左;
console.log(Direction.上, Direction.下, Direction.左, Direction.右);
console.log(Level.中级, Level[2]); // 数字枚举有"反向映射"
console.log(Status.Done, Status["待处理"]);
console.log(Calc.A, Calc.B, Calc.C);
// 枚举本质是个对象,可以遍历
console.log(Object.keys(Status), Object.values(Status));
function canRetry(s: Status): boolean {
return s !== Status.Done;
}
console.log(canRetry(Status.Failed), canRetry(Status.Done));
要注意:只有数字枚举有反向映射(Level[2] 得到 "中级"),字符串枚举没有。const enum 会被直接内联成字面量,运行时对象不存在,所以不能 Object.keys(const enum)。
数组、元组与只读
// 数组的两种等价写法
const a: number[] = [1, 2, 3];
const b: Array<string> = ["甲", "乙"];
// 元组:长度和每个位置的类型都固定
const point: [number, number] = [3, 4];
const row: [number, string, boolean] = [1, "小明", true];
// 可以命名字段,读起来更清楚
const range: [start: number, end: number] = [1, 10];
// 只读数组:不能 push / 不能改元素
const frozen: readonly number[] = [9, 8, 7];
const asConst = [1, 2, 3] as const; // as const 让每个元素都变成字面量类型且只读
// 常见操作和 JavaScript 完全一样
const nums = [5, 3, 9, 1];
const doubled: number[] = nums.map((n: number): number => n * 2);
const evens: number[] = nums.filter((n) => n % 2 === 1);
console.log(a, b);
console.log(point, row, range);
console.log("解构元组:", point[0], point[1]);
const [x, y] = point;
console.log(x, y);
console.log(frozen, asConst);
console.log(doubled, evens, nums.reduce((s, n) => s + n, 0));
要注意:readonly number[] 和 as const 的限制也只是编译期的,运行时它们还是普通数组,用 push 一样能塞进去。
类型断言与非空断言
// as:告诉编译器"我确定它是什么类型"
const raw: unknown = "本站是 run.ban.show";
const len = (raw as string).length;
// 尖括号写法等价,但在 .tsx 里不能用
const len2 = (<string>raw).length;
// 非空断言 !:告诉编译器"这个值一定不是 null / undefined"
function findName(list: string[], target: string): string {
const found = list.find((s) => s === target);
return found!.toUpperCase(); // 找不到时运行时依然是 undefined
}
// 更安全的写法:先判断
function safeFind(list: string[], target: string): string {
const found = list.find((s) => s === target);
return found ? found.toUpperCase() : "(没找到)";
}
const list = ["语文", "数学", "英语"];
console.log(len, len2);
console.log(safeFind(list, "数学"), safeFind(list, "物理"));
console.log("非空断言找不到时:", findName(list, "数学"));
// 断言不能凭空改变运行时的值
const n = "123" as unknown as number;
console.log(typeof n, n + 1); // 仍然是字符串,"123" + 1 = "1231"
要注意:断言只是「让编译器闭嘴」,不会做任何转换。上面 "123" as unknown as number 得到的还是字符串,所以 n + 1 是 "1231" 而不是 124。这是初学者最容易踩的坑。
keyof、typeof 与索引类型
const config = {
host: "run.ban.show",
port: 443,
https: true,
};
// typeof 取一个值的类型,keyof 取它所有键组成的联合类型
type Config = typeof config;
type ConfigKey = keyof Config; // "host" | "port" | "https"
// 索引类型:T[K] 取出某个键对应的类型
function get<K extends ConfigKey>(key: K): Config[K] {
return config[key];
}
// 只允许对象里已有的键,避免拼错
function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = obj[k];
return out;
}
interface User {
id: number;
name: string;
age: number;
email: string;
}
const users: User[] = [
{ id: 1, name: "小明", age: 18, email: "xm@example.com" },
{ id: 2, name: "小红", age: 20, email: "xh@example.com" },
];
console.log(get("host"), get("port"), get("https"));
console.log(pick(users[0], ["name", "age"]));
// Partial / Required / Omit / Record 这些内置工具类型也能用
type Draft = Partial<User>;
type PublicUser = Omit<User, "email">;
type ById = Record<number, string>;
const draft: Draft = { name: "小刚" };
const pub: PublicUser = { id: 3, name: "小刚", age: 17 };
const names: ById = { 1: "小明", 2: "小红" };
console.log(draft, pub, names);
要注意:keyof 得到的是「键的联合类型」,不是数组。想拿到键的数组要用运行时的 Object.keys(config)。
可选链、空值合并与类型收窄
interface Company {
name: string;
address?: { city: string; zip?: string };
contacts?: string[];
}
const c1: Company = { name: "甲公司", address: { city: "北京" } };
const c2: Company = { name: "乙公司" };
// ?. 遇到 null / undefined 就短路返回 undefined,不会报错
console.log(c1.address?.city, c1.address?.zip);
console.log(c2.address?.city); // undefined,而不是抛 TypeError
console.log(c1.contacts?.[0], c1.contacts?.length);
// ?? 只在左边是 null / undefined 时取右边(|| 还会把 0、"" 也换掉)
console.log(c1.address?.zip ?? "无邮编");
console.log(0 || "默认值", 0 ?? "默认值");
// 可空类型的收窄
function showCity(c: Company): string {
if (c.address === undefined) return `${c.name}:地址未知`;
return `${c.name}:${c.address.city}`; // 这一行里 address 已经确定存在
}
console.log(showCity(c1));
console.log(showCity(c2));
// 用 in 收窄、用 instanceof 收窄
type Dog = { kind: "dog"; bark(): void };
type Cat = { kind: "cat"; meow(): void };
function act(a: Dog | Cat) {
if ("bark" in a) a.bark();
else a.meow();
}
act({ kind: "dog", bark: () => console.log("汪!") });
act({ kind: "cat", meow: () => console.log("喵~") });
class MyError extends Error {}
function handle(e: unknown) {
console.log(e instanceof MyError ? "自定义错误" : "其它错误");
}
handle(new MyError("x"));
handle("字符串错误");
异步与 Promise
// Promise<T> 里的 T 是"成功时的值的类型"
function wait(ms: number, value: string): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
// async 函数的返回类型永远是 Promise
async function loadUser(id: number): Promise<{ id: number; name: string }> {
const raw = await wait(120, `用户${id}`);
return { id, name: raw };
}
// 顶层 await 可以直接用,不用包在 async 函数里
const u = await loadUser(1);
console.log("单个:", u);
const t0 = Date.now();
const all = await Promise.all([loadUser(2), loadUser(3), loadUser(4)]);
console.log("并发:", all, `${Date.now() - t0}ms`);
// 带类型的错误处理
try {
await new Promise<never>((_, reject) => setTimeout(() => reject(new Error("超时")), 50));
} catch (e) {
// catch 到的 e 类型是 unknown,用之前要判断
console.log("捕获:", e instanceof Error ? e.message : String(e));
}
// 给回调标类型
const tasks: Array<() => Promise<number>> = [
async () => 1,
async () => 2,
];
for (const task of tasks) console.log("结果:", await task());
satisfies 与 as const
// satisfies:既检查值符合某个类型,又保留更精确的字面量类型
type Theme = Record<string, string | [string, string]>;
const theme = {
primary: "#3b82f6",
danger: ["#ef4444", "#7f1d1d"],
} satisfies Theme;
// 因为没被"拓宽"成 string,这里还能拿到精确的类型
console.log(theme.primary.toUpperCase());
console.log(theme.danger[0], Array.isArray(theme.danger));
// as const:把对象变成深度只读 + 字面量类型
const ROUTES = {
home: "/",
python: "/python/",
sql: "/sql/",
} as const;
type RouteKey = keyof typeof ROUTES;
function go(key: RouteKey): string {
return ROUTES[key];
}
console.log(ROUTES.home, go("python"), go("sql"));
console.log(Object.entries(ROUTES));
// 常见搭配:用 as const 数组做"枚举",比 enum 更轻
const LANGS = ["python", "javascript", "typescript", "sql"] as const;
type Lang = (typeof LANGS)[number];
function isLang(v: string): v is Lang {
return (LANGS as readonly string[]).includes(v);
}
console.log(isLang("sql"), isLang("rust"));
console.log(LANGS.filter((l) => l.length > 3));
要注意:v is Lang 这种写法叫「类型谓词」,是自定义类型收窄的方式。本站不做类型检查,所以它运行时就是一个普通函数,返回 true / false。