本节目标:读完这一节,你能说清 TypeScript 有哪几种原始类型、它们分别对应什么运行时值;能熟练写出变量、参数、返回值的类型注解;能判断一段代码是该显式标注还是交给推断;并且能读懂
Type 'string' is not assignable to type 'number'这类最常见的报错到底在说什么。
3.1 原始类型与类型注解
如果把 TypeScript 比作一门「给 JavaScript 加装安全带」的语言,那么原始类型就是这条安全带最基础的织线。它看起来简单——无非是 string、number、boolean 这几个单词——但正是这些单词决定了编译器能不能在你按下保存键的那一刻就发现错误。
在 1.2 与 JavaScript 的关系(超集·类型擦除) 中我们已经强调过:TypeScript 的类型在编译后会被完全擦除,运行时看到的仍然是普通的 JavaScript。这意味着类型注解是纯粹的「开发期契约」,它不产生任何运行时开销,也无法阻止外部数据(比如接口返回的 JSON)违反契约。理解这一点,你才不会对类型系统产生不切实际的期待。
为什么需要类型注解
先看一段纯 JavaScript:
function totalPrice(price, count) {
return price * count;
}
totalPrice("19.9", 3); // 59.699999999999996 —— 字符串被隐式转成了数字
这段代码不会报错,但结果未必是你想要的。如果 price 本应是数字,却被传入了 "19.9 元",结果就是 NaN,而错误会一路漂移到很远的地方才暴露出来。
加上注解之后:
function totalPrice(price: number, count: number): number {
return price * count;
}
totalPrice("19.9", 3);
// ❌ Argument of type 'string' is not assignable to parameter of type 'number'.
错误被提前到了编译期,而且指哪打哪——编译器直接告诉你第几个参数不匹配。这就是类型注解的全部价值:把运行时的「玄学 bug」变成编辑期的「红色波浪线」。
七种原始类型
TypeScript 的原始类型与 JavaScript 的 typeof 结果基本一一对应,但有几个细节差异需要注意。
| 类型 | 对应运行时值 | 说明 | 常见坑 |
|---|---|---|---|
string | typeof x === "string" | 文本,含模板字符串 | 单双引号与反引号在类型上无差别 |
number | typeof x === "number" | 整数、浮点、NaN、Infinity 统统是它 | 没有 int / float / double 之分 |
boolean | typeof x === "boolean" | 只有 true / false | 0 / "" 不是 boolean |
null | typeof x === "object" | 显式的「空」 | 只有开启严格空值检查才与 undefined 区分 |
undefined | typeof x === "undefined" | 未赋值 | 同上 |
symbol | typeof x === "symbol" | 唯一标识符 | 常用于 unique symbol 做品牌类型 |
bigint | typeof x === "bigint" | 任意精度整数 | 不能与 number 混算 |
基础写法:
let userName: string = "Ada";
let age: number = 36;
let isActive: boolean = true;
let big: bigint = 9007199254740993n; // 超出 Number.MAX_SAFE_INTEGER 也能精确表示
const id: unique symbol = Symbol("id");
有两个地方零基础读者最容易踩:
第一,number 不区分整数与浮点。 很多人从 Java/C# 过来,习惯写 int、double,在 TypeScript 里这些都是 number。如果你真的需要区分「整数」,只能用字面量类型或品牌类型在类型层面模拟,运行时依然是同一个 number。
第二,bigint 与 number 不能直接运算。 下面这段代码会报错:
const a: bigint = 1n;
const b: number = 2;
console.log(a + b);
// ❌ Operator '+' cannot be applied to types 'bigint' and 'number'.
必须显式转换:a + BigInt(b) 或者 Number(a) + b。
类型注解的三种位置
类型注解使用冒号 : 引入,出现在三个地方:变量、函数参数、函数返回值。
// 1. 变量
let retryCount: number = 3;
// 2. 函数参数 + 返回值
function formatPrice(amount: number, currency: string): string {
return `${currency} ${amount.toFixed(2)}`;
}
// 3. 箭头函数(返回值注解写在参数括号之后)
const toSlug = (title: string): string =>
title.trim().toLowerCase().replace(/\s+/g, "-");
书写风格上有几条社区共识值得从第一天就养成:
// ✅ 冒号后有一个空格,与变量名之间没有空格
let count: number = 0;
// ❌ 这两种都不推荐
let a : number = 0;
let b:number = 0;
返回值注解看起来啰嗦,但在导出函数上强烈建议写。原因有两个:一是它把函数的契约固定下来,函数体改动时如果破坏了契约会立刻报错;二是它让编辑器的类型提示不必深入函数体就能给出准确结果,大型项目里能明显加快检查速度。
类型推断:什么时候可以不写
TypeScript 的类型推断相当聪明,绝大多数局部变量根本不需要手写注解。
let count = 0; // 推断为 number
let title = "hello"; // 推断为 string
let flags = [true]; // 推断为 boolean[]
count = "hello";
// ❌ Type 'string' is not assignable to type 'number'.
既然不写也能推断,那什么时候必须写?记住下面三条经验规则:
规则一:函数参数必须写(或依赖上下文推断)。 函数参数没有「初始值」,编译器无从推断。
function greet(name) {
// ❌ Parameter 'name' implicitly has an 'any' type.
return `Hello, ${name}`;
}
这条报错来自 noImplicitAny(包含在 strict 里,见 16.1 编译目标与严格模式配置
)。永远不要用关闭 noImplicitAny 的方式绕过它,那等于把 TypeScript 降级成 JavaScript。
规则二:想让变量的类型比推断结果更宽或更窄时,要写。 看这个经典对比:
const role = "admin"; // 推断为字面量类型 "admin"
let role2 = "admin"; // 推断为 string
const 声明的变量不可能被重新赋值,所以编译器把类型收窄到字面量 "admin";let 可能被改写成别的字符串,于是推断为宽泛的 string。这个差异在 7.1 联合类型与字面量类型
里会大放异彩,现在先记住它。
规则三:对象字面量在需要精确结构时写。 空对象和空数组是推断的盲区:
const config = {};
// 推断为 {},之后 config.port = 3000 会报错
const list = [];
// 推断为 any[],元素类型完全失控
正确做法是显式标注:
const config: { port: number; host: string } = { port: 3000, host: "localhost" };
const list: string[] = [];
上下文推断:注解也可以「省略」
有些场景下,即使你不写注解,编译器也能从使用位置反推出类型,这叫做上下文推断(contextual typing)。
const names = ["ada", "linus", "grace"];
names.forEach((name) => {
// name 被推断为 string,无需手写
console.log(name.toUpperCase());
});
forEach 的签名声明了回调参数是 string,编译器把这个信息「传」给了箭头函数。理解上下文推断能让你少写大量注解,同时不损失任何类型安全。
原始类型之间的转换
JavaScript 的隐式转换规则诡异,TypeScript 不会阻止你写出它们,但会在结果类型上给出提示。
const n = Number("42"); // number
const s = String(42); // string
const b = Boolean(""); // boolean —— 注意 "" 转成 false
const p = parseInt("42px", 10); // number —— 42,遇到非数字字符停止
const f = parseFloat("3.14abc"); // number —— 3.14
const nan = Number("abc"); // number —— NaN,但类型仍然是 number!
最后一行值得警惕:NaN 的类型是 number,所以类型系统无法帮你拦住它。这也是为什么 13.2 Zod 模式验证与类型推导
这类运行时校验在数值边界上不可替代。
+ 运算符是另一个高频坑:
const a = 1 + "2"; // string,"12"
const b = 1 + 2; // number,3
const c = "1" - 0; // number,1 —— 减法会把字符串强制转成数字
显式转换永远优于隐式转换。类型注解拦不住 1 + "2"(两边类型都合法),但写成 String(x) / Number(x) 能让意图一目了然。
在编辑器里验证推断结果
「不写注解时编译器到底推断成了什么」,是学习期最需要确认的信息。有三种确认方式。
第一种是命令行做纯类型检查,不产出任何文件:
npx tsc --noEmit # 只做类型检查,不产出 .js
第二种是编辑器悬停。把鼠标停在变量上,VS Code 会弹出编译器推断出的类型:
const config = { port: 3000, host: "localhost" };
// 悬停 config,提示为:
// const config: { port: number; host: string; }
第三种是「故意写错」,让报错信息反过来暴露真实类型:
const flags = [true, false];
const wrong: string = flags;
// ❌ Type 'boolean[]' is not assignable to type 'string'.
// 报错里就暴露了 flags 的真实类型:boolean[]
养成「先看推断、再决定要不要注解」的习惯,你的注解会写得少而准。
空值:null 与 undefined 的严格检查
在默认(非严格)配置下,null 和 undefined 可以赋值给任何类型,这是 JavaScript 历史遗留的「十亿美元错误」。开启 strictNullChecks 之后,情况彻底改变:
// tsconfig.json: "strict": true
let maybe: string | null = null;
console.log(maybe.length);
// ❌ 'maybe' is possibly 'null'.
你必须先收窄:
if (maybe !== null) {
console.log(maybe.length); // ✅ 这里 maybe 是 string
}
关于 strictNullChecks 的完整规则和它与 null 相关的类型收窄技巧,我们会在 13.1 类型擦除带来的运行时盲区
里结合真实数据流再讲一遍。
常见报错速查
新手期会反复遇到下面这几条报错,看懂它们能省下大量搜索时间。
| 报错信息 | 含义 | 修法 |
|---|---|---|
Type 'string' is not assignable to type 'number' | 赋值类型不兼容 | 改值,或改注解 |
Parameter 'x' implicitly has an 'any' type | 参数缺注解且开了 noImplicitAny | 补注解 |
'x' is possibly 'null' | 严格空值检查拦下了可能的空值 | 先判断再访问 |
Variable 'x' is used before being assigned | 声明了但某条分支没赋值 | 补初始值或改控制流 |
Cannot find name 'x' | 变量未声明,或缺少类型声明包 | 检查拼写 / 装 @types/* |
顺带提一个「看起来像 bug 其实不是」的例子:
let data; // 推断为 any —— 但这是「演化型 any」,不报错
data = 1;
data = "hello"; // 允许,data 的类型会随赋值演化
let data; 这种没有初始值也没有注解的写法叫「隐式演化 any」,TypeScript 允许它存在,但一旦你把值读出来用,它就退化成普通 any。生产代码里请永远写上初始值。
与运行时校验的分工
最后必须重复一个认知:类型注解只管编译期。下面这段代码编译完全通过,运行时会崩:
const raw = '{"port": "3000"}'; // 来自接口,port 是字符串
const config = JSON.parse(raw) as { port: number };
config.port.toFixed(0);
// 💥 TypeError: config.port.toFixed is not a function
JSON.parse 返回 any,as 只是你的口头保证,编译器不会去验证。真正安全的做法是在边界处做运行时校验——这个话题属于 13.2 Zod 模式验证与类型推导
,如果你现在就想了解工程界的成熟方案,可以先读站内的 TypeScript 严格模式配置指南
。
小结
这一节我们把地基铺好了:
- TypeScript 有七种原始类型:
string、number、boolean、null、undefined、symbol、bigint;number不区分整浮点,bigint不能与number混算。 - 类型注解写在变量、参数、返回值三处,冒号后加空格;导出函数建议写返回值注解。
- 类型推断能覆盖大多数局部变量,但函数参数必须标注,空对象与空数组必须标注。
strictNullChecks让null/undefined不再能随便赋值,这是安全性的关键开关。- 类型注解只作用于编译期,运行时边界仍然需要校验。
掌握了原始类型,接下来我们要面对「一组同类型值」和「一组固定顺序的不同类型值」这两类需求。下一节 3.2 数组、元组与枚举
会讲清数组的两种写法、元组的精确定位能力,以及枚举为什么在现代 TypeScript 里常常被 as const 对象取代。
阅读导航:上一节:2.3 第一个项目:从零到运行 · 下一节:3.2 数组、元组与枚举 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。