在线代码运行BanShow Run

TypeScript 常见报错与原因

TypeScript 常见报错对照表,每一节标题就是你会在屏幕上看到的报错原文,下面给出原因和改法。可以复制到浏览器里亲手触发一遍,比背教程记得牢。

打开 TypeScript 运行器 →

下面这些代码都会报错,是故意写出来给你看报错长什么样的。复制到编辑器里点运行,对照输出区的信息读一遍,比看十遍教程有用。

编译报错怎么读

本站的 TypeScript 是先在浏览器里编译成 JavaScript 再运行的,所以报错分两类:编译错误(语法写错了,一行都不会执行)和运行时错误(语法没问题,跑起来才出错)。

编译错误的格式是 Unexpected token, expected ";" (2:7),括号里的 (2:7) 表示第 2 行第 7 列。看行号,然后去那一行找少写的括号、逗号或分号。

typescript
interface User {
  name: string        // 这一行末尾少了一个右大括号
const u: User = { name: "小明" };
console.log(u.name);

输出:Unexpected token, expected ";" (3:7)。

注意报错说的是第 3 行,可你漏写 } 的地方在第 2 行——因为少了一个 },编译器把第 3 行也当成了接口的一部分,一路读到 u 这个位置(第 3 行第 7 列)才发现读不下去了。报错行号往往不是你写错的那一行,而是编译器"终于看不懂"的那一行。

少写括号、逗号

typescript
function add(a: number, b: number): number {
  return a + b;
}
console.log(add(1, 2);     // 少了一个右括号

输出:Unexpected token, expected "," (4:22)。第 4 行第 22 列正好是那个 ;:编译器在参数列表里读到分号,它期待的是 , 或者 )。所以 expected "," 这类提示的意思是「我在这里读到了不该出现的东西」。

类型注解的位置写错

typescript
let name: = "小明";
console.log(name);

输出:Unexpected token (1:11)。第 1 行第 11 列是 =:冒号后面本该是类型,结果直接碰到了等号。记住顺序永远是 变量名: 类型 = 值。

另一种更隐蔽的写法是把类型和变量名写反:

typescript
let string: name = "小明";
console.log(string, typeof string);
console.log(name);

前两句能正常打印 小明 string——因为这被解析成「声明一个叫 string 的变量,类型是 name」,类型注解在运行时被删掉了,所以根本不检查。直到第 3 行才炸:ReferenceError: name is not defined。

本站不做类型检查

这是最需要理解的一点。本站只做「把类型注解擦掉,然后运行 JavaScript」,不会检查类型是否匹配。所以下面这段能正常运行:

typescript
const a: number = "我是一个字符串";
const b: string = 123;
console.log(a, b);
console.log("类型注解在运行时已经不存在了");

它不报错,但真到用的时候就炸了:

typescript
const price: number = "19.9";        // 标成 number,实际是字符串
console.log(price.toFixed(2));       // TypeError: price.toFixed is not a function

结论:在本站验证 TypeScript,能确认「语法能不能编译、逻辑跑得对不对」,不能确认「类型对不对」。要检查类型请在本地用 tsc --noEmit。

private 和 readonly 只是编译期的

typescript
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 的 # 语法:

typescript
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 不支持

typescript
namespace Utils {
  export const VERSION = "1.0";
}
console.log(Utils.VERSION);

输出:ReferenceError: Utils is not defined。编译器把 namespace 块整个擦掉了,但引用它的代码还在。请改用 ES 模块的思路:直接写顶层的 const / function,或者用一个对象字面量代替。

typescript
// 正确写法
const Utils = {
  VERSION: "1.0",
  greet: (name: string) => `你好,${name}`,
};
console.log(Utils.VERSION, Utils.greet("小明"));

装饰器(decorator)不支持

装饰器有两种翻车方式,写在方法上的那种更阴险——它不报错,只是被悄悄删掉了:

typescript
function Log(target: any, key: string) {
  console.log("装饰器触发了:", key); // 这一行永远不会执行
}

class A {
  @Log
  greet(name: string) {
    return "你好," + name;
  }
}
console.log(new A().greet("小明"));

输出只有一行 你好,小明,没有「装饰器触发了」。编译器把 @Log 直接擦掉了,程序照常跑完,但装饰器一次都没执行。这比报错更坑——你会以为自己的装饰器写对了。

写在类上的装饰器是另一种表现:编译器把 @Sealed 原样留着,浏览器读到 @ 就报语法错误,而且给不出行号:

typescript
function Sealed(c: any) {}

@Sealed
class B {}

输出:SyntaxError: Invalid or unexpected token,行列号都是 0。

怎么办:装饰器请在本地用 tsc 验证。在本站想看到等价的效果,把「装饰」写成一次普通调用就行:

typescript
// 行为等价,而且看得见执行过程
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("小明"));

输出:

text
装饰器触发了: greet
你好,小明

import / require 不支持

沙箱里没有模块系统,不能加载任何外部文件或 npm 包。

typescript
import { format } from "date-fns";
console.log(format(new Date(), "yyyy-MM-dd"));

输出:本站不支持加载外部模块:date-fns。需要用的代码请直接写在同一个文件里。

typescript
// 正确写法:自己实现
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

typescript
const names = ["语文", "数学"];
const found = names.find((n) => n === "英语");
console.log(found!.toUpperCase());      // ! 说"一定不是 undefined",但其实是

输出:TypeError: Cannot read properties of undefined (reading 'toUpperCase')。

! 不会做任何检查,它只是让编译器不报警。能不用就不用,改成先判断:

typescript
const names = ["语文", "数学"];
const found = names.find((n) => n === "英语");
if (found !== undefined) {
  console.log(found.toUpperCase());
} else {
  console.log("没找到,用默认值:", (found ?? "无").toString());
}

as 断言不会转换值

typescript
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):

typescript
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")));

可选属性没判断就访问

typescript
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')。可选属性一定要先判断,或者用 ?. 和 ??:

typescript
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

typescript
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 在运行时不存在

typescript
interface User { name: string }
const u = { name: "小明" };
console.log(u instanceof User);

输出:ReferenceError: User is not defined。interface 和 type 编译后完全消失,所以不能用 instanceof。想在运行时判断结构,只能自己检查字段:

typescript
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("字符串"));

泛型擦除后拿不到类型

typescript
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 很容易被忽略。泛型只在编译期存在。运行时要知道类型,得把它当参数传进来:

typescript
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"));

枚举的坑

typescript
// 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 更省心:

typescript
const Level = { Low: 1, High: 2 } as const;
console.log(Level.High, Object.keys(Level), Object.values(Level));

忘记写返回类型导致返回 undefined

typescript
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 了。

父类构造函数里调用被子类覆盖的方法

typescript
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 之后自己调用。

定位报错的三个习惯

  1. 先看是编译错误还是运行时错误。 有 (行:列) 的是编译错误,改语法;有 TypeError / ReferenceError 的是运行时错误,改逻辑。
  2. 编译错误从最小的括号开始查。 80% 的 Unexpected token 是少了一个 }、) 或 ,。把光标放在报错行的上一行末尾看。
  3. **怀疑类型时用 console.log(typeof x, x) 打印真实值。** 因为本站不做类型检查,类型注解写的和运行时实际的可能完全不是一回事。

换一门语言