下面这些代码都会报错,是故意写出来给你看报错长什么样的。复制到编辑器里点运行,对照输出区的信息读一遍,比看十遍教程有用。
编译报错怎么读
本站的 TypeScript 是先在浏览器里编译成 JavaScript 再运行的,所以报错分两类:编译错误(语法写错了,一行都不会执行)和运行时错误(语法没问题,跑起来才出错)。
编译错误的格式是 Unexpected token, expected ";" (2:7),括号里的 (2:7) 表示第 2 行第 7 列。看行号,然后去那一行找少写的括号、逗号或分号。
interface User {
name: string // 这一行末尾少了一个右大括号
const u: User = { name: "小明" };
console.log(u.name);
输出:Unexpected token, expected ";" (3:7)。
注意报错说的是第 3 行,可你漏写 } 的地方在第 2 行——因为少了一个 },编译器把第 3 行也当成了接口的一部分,一路读到 u 这个位置(第 3 行第 7 列)才发现读不下去了。报错行号往往不是你写错的那一行,而是编译器"终于看不懂"的那一行。
少写括号、逗号
function add(a: number, b: number): number {
return a + b;
}
console.log(add(1, 2); // 少了一个右括号
输出:Unexpected token, expected "," (4:22)。第 4 行第 22 列正好是那个 ;:编译器在参数列表里读到分号,它期待的是 , 或者 )。所以 expected "," 这类提示的意思是「我在这里读到了不该出现的东西」。
类型注解的位置写错
let name: = "小明";
console.log(name);
输出:Unexpected token (1:11)。第 1 行第 11 列是 =:冒号后面本该是类型,结果直接碰到了等号。记住顺序永远是 变量名: 类型 = 值。
另一种更隐蔽的写法是把类型和变量名写反:
let string: name = "小明";
console.log(string, typeof string);
console.log(name);
前两句能正常打印 小明 string——因为这被解析成「声明一个叫 string 的变量,类型是 name」,类型注解在运行时被删掉了,所以根本不检查。直到第 3 行才炸:ReferenceError: name is not defined。
本站不做类型检查
这是最需要理解的一点。本站只做「把类型注解擦掉,然后运行 JavaScript」,不会检查类型是否匹配。所以下面这段能正常运行:
const a: number = "我是一个字符串";
const b: string = 123;
console.log(a, b);
console.log("类型注解在运行时已经不存在了");
它不报错,但真到用的时候就炸了:
const price: number = "19.9"; // 标成 number,实际是字符串
console.log(price.toFixed(2)); // TypeError: price.toFixed is not a function
结论:在本站验证 TypeScript,能确认「语法能不能编译、逻辑跑得对不对」,不能确认「类型对不对」。要检查类型请在本地用 tsc --noEmit。
private 和 readonly 只是编译期的
class Account {
constructor(private balance: number, readonly owner: string) {}
}
const acc = new Account(100, "小明");
// 编译期不允许,但运行时完全读得到
console.log(acc.balance, acc.owner);
acc.balance = 99999;
console.log("改了只读/私有属性:", acc.balance);
不会报错,能打印出 99999。private / readonly 编译成 JavaScript 后就消失了。想要运行时真正私有,用 JavaScript 的 # 语法:
class Safe {
#balance = 100;
get balance() { return this.#balance; }
}
const s = new Safe();
console.log("初始:", s.balance);
try {
s.balance = 1; // 只有 getter 没有 setter,严格模式下抛 TypeError
} catch (e) {
console.log("尝试修改:", e.constructor.name + ":", e.message);
}
console.log("修改后:", s.balance);
console.log("# 字段在实例上枚举不到:", Object.keys(s), JSON.stringify(s));
输出:初始 100 → TypeError: Cannot set property balance of #<Safe> which has only a getter → 修改后仍然是 100。#balance 也不会出现在 Object.keys 和 JSON.stringify 里。
namespace 不支持
namespace Utils {
export const VERSION = "1.0";
}
console.log(Utils.VERSION);
输出:ReferenceError: Utils is not defined。编译器把 namespace 块整个擦掉了,但引用它的代码还在。请改用 ES 模块的思路:直接写顶层的 const / function,或者用一个对象字面量代替。
// 正确写法
const Utils = {
VERSION: "1.0",
greet: (name: string) => `你好,${name}`,
};
console.log(Utils.VERSION, Utils.greet("小明"));
装饰器(decorator)不支持
装饰器有两种翻车方式,写在方法上的那种更阴险——它不报错,只是被悄悄删掉了:
function Log(target: any, key: string) {
console.log("装饰器触发了:", key); // 这一行永远不会执行
}
class A {
@Log
greet(name: string) {
return "你好," + name;
}
}
console.log(new A().greet("小明"));
输出只有一行 你好,小明,没有「装饰器触发了」。编译器把 @Log 直接擦掉了,程序照常跑完,但装饰器一次都没执行。这比报错更坑——你会以为自己的装饰器写对了。
写在类上的装饰器是另一种表现:编译器把 @Sealed 原样留着,浏览器读到 @ 就报语法错误,而且给不出行号:
function Sealed(c: any) {}
@Sealed
class B {}
输出:SyntaxError: Invalid or unexpected token,行列号都是 0。
怎么办:装饰器请在本地用 tsc 验证。在本站想看到等价的效果,把「装饰」写成一次普通调用就行:
// 行为等价,而且看得见执行过程
function Log(target: any, key: string) {
console.log("装饰器触发了:", key);
}
class A {
greet(name: string) {
return "你好," + name;
}
}
Log(A.prototype, "greet");
console.log(new A().greet("小明"));
输出:
装饰器触发了: greet
你好,小明
import / require 不支持
沙箱里没有模块系统,不能加载任何外部文件或 npm 包。
import { format } from "date-fns";
console.log(format(new Date(), "yyyy-MM-dd"));
输出:本站不支持加载外部模块:date-fns。需要用的代码请直接写在同一个文件里。
// 正确写法:自己实现
function formatDate(d: Date): string {
const p = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
}
console.log(formatDate(new Date(2026, 0, 5)));
非空断言 ! 掩盖了 undefined
const names = ["语文", "数学"];
const found = names.find((n) => n === "英语");
console.log(found!.toUpperCase()); // ! 说"一定不是 undefined",但其实是
输出:TypeError: Cannot read properties of undefined (reading 'toUpperCase')。
! 不会做任何检查,它只是让编译器不报警。能不用就不用,改成先判断:
const names = ["语文", "数学"];
const found = names.find((n) => n === "英语");
if (found !== undefined) {
console.log(found.toUpperCase());
} else {
console.log("没找到,用默认值:", (found ?? "无").toString());
}
as 断言不会转换值
const input = "19.9";
const price = input as unknown as number;
console.log(price * 2); // "19.9" * 2 —— 恰好能算,因为 * 会隐式转换
console.log(price.toFixed(2)); // TypeError: price.toFixed is not a function
断言只改变编译器认为的类型,不改变运行时的值。要真的转换请用 Number(input):
const input = "19.9";
const price = Number(input);
console.log(price * 2, price.toFixed(2), Number.isNaN(price));
console.log("转不成数字时:", Number("abc"), Number.isNaN(Number("abc")));
可选属性没判断就访问
interface Config {
host: string;
port?: number; // 可选
}
const c: Config = { host: "localhost" };
console.log(c.port.toFixed(0)); // port 是 undefined
输出:TypeError: Cannot read properties of undefined (reading 'toFixed')。可选属性一定要先判断,或者用 ?. 和 ??:
interface Config { host: string; port?: number }
const c: Config = { host: "localhost" };
console.log(c.port?.toFixed(0) ?? "使用默认端口");
console.log((c.port ?? 8080).toFixed(0));
抽象类在运行时能 new
abstract class Shape {
abstract area(): number;
}
const s = new Shape(); // 编译期禁止,运行时不禁止
console.log(s);
console.log(s.area()); // 抽象方法根本没被生成
new Shape() 不报错(打印出一个空对象),但 s.area() 会抛 TypeError: s.area is not a function——因为 abstract area(): number; 只是一句声明,编译后什么都不剩。
interface 在运行时不存在
interface User { name: string }
const u = { name: "小明" };
console.log(u instanceof User);
输出:ReferenceError: User is not defined。interface 和 type 编译后完全消失,所以不能用 instanceof。想在运行时判断结构,只能自己检查字段:
interface User { name: string; age: number }
function isUser(v: unknown): v is User {
return typeof v === "object" && v !== null
&& typeof (v as User).name === "string"
&& typeof (v as User).age === "number";
}
console.log(isUser({ name: "小明", age: 18 }));
console.log(isUser({ name: "小明" }));
console.log(isUser("字符串"));
泛型擦除后拿不到类型
function make<T>(value: T): T {
// 想在运行时判断 T 是什么类型 —— 做不到,T 已经被擦掉了
console.log(typeof T); // 打印 undefined,因为 T 这个变量根本不存在
return value;
}
console.log(make(1));
不会报错,但 typeof T 打印的是 "undefined"——typeof 对未声明的变量不会抛异常,只会安静地返回 "undefined",所以这个 bug 很容易被忽略。泛型只在编译期存在。运行时要知道类型,得把它当参数传进来:
function parse<T>(text: string, kind: "number" | "json"): T | null {
try {
return (kind === "number" ? Number(text) : JSON.parse(text)) as T;
} catch {
return null;
}
}
console.log(parse<number>("42", "number"));
console.log(parse<{ a: number }>('{"a":1}', "json"));
console.log(parse<number>("abc", "json"));
枚举的坑
// 1. 字符串枚举没有反向映射
enum Status { Done = "已完成" }
console.log(Status.Done); // "已完成"
console.log(Status["已完成"]); // undefined
// 2. const enum 别指望能遍历
const enum Level { Low = 1, High = 2 }
console.log(Level.High); // 2
console.log(Object.keys(Level)); // 本站能打印出来,但用标准 tsc 编译时会报错
两点:只有数字枚举有反向映射,字符串枚举没有;const enum 在标准 TypeScript 编译器(tsc)里会被内联成字面量、运行时不生成对象,所以 Object.keys(Level) 在真项目里会直接崩。本站的编译器为了兼容性保留了对象,但你不能依赖这个差异。需要遍历键值时,用普通对象 + as const 更省心:
const Level = { Low: 1, High: 2 } as const;
console.log(Level.High, Object.keys(Level), Object.values(Level));
忘记写返回类型导致返回 undefined
function total(prices: number[]): number {
let sum = 0;
for (const p of prices) {
sum += p;
// 忘了写 return sum
}
}
console.log(total([1, 2, 3]).toFixed(2));
输出:TypeError: Cannot read properties of undefined (reading 'toFixed')。标了 : number 也不会帮你检查是否真的 return 了。
父类构造函数里调用被子类覆盖的方法
class Base {
greeting = "你好";
constructor() {
this.init(); // 父类构造函数里调用被子类覆盖的方法
}
init() { console.log("Base.init"); }
}
class Child extends Base {
extra = "子类字段";
init() { console.log("Child.init,此时 extra =", this.extra, "greeting =", this.greeting); }
}
new Child();
不报错,但打印的是 Child.init,此时 extra = undefined greeting = 你好。
原因是字段的初始化顺序:父类字段 → 父类构造函数体 → 子类字段。super() 执行完之前,子类的字段一个都还没赋值,而 this.init() 因为多态调到的是子类版本,于是它看到了 undefined。
规则:不要在构造函数里调用可能被子类覆盖的方法。要初始化就放在一个显式的 init() 里,new 之后自己调用。
定位报错的三个习惯
- 先看是编译错误还是运行时错误。 有
(行:列)的是编译错误,改语法;有TypeError/ReferenceError的是运行时错误,改逻辑。 - 编译错误从最小的括号开始查。 80% 的
Unexpected token是少了一个}、)或,。把光标放在报错行的上一行末尾看。 - **怀疑类型时用
console.log(typeof x, x)打印真实值。** 因为本站不做类型检查,类型注解写的和运行时实际的可能完全不是一回事。