前置回顾
- 第 01 篇装好了 Node.js + pnpm + TypeScript 工具链
- 用
pnpm exec tsc编译,pnpm exec tsx直接运行.ts文件 - 本篇开始写真正的 TS 代码, 从变量声明和类型系统入手
一句话总结
TS 用 const/let 声明变量(不用 var), 类型标注写在变量名后面(name: type), 大多数情况编译器能自动推断类型不需要手写.
变量声明: const, let, var
TS 有三种变量声明关键字. 实际只用前两个:
|
const 优先原则
和 Rust 的 let 默认不可变一样, TS 的最佳实践是默认用 const, 需要改值时才用 let.
|
| 语言 | 不可变 | 可变 |
|---|---|---|
| Rust | let x = 1 |
let mut x = 1 |
| Go | const x = 1(仅编译期常量) |
x := 1 / var x = 1 |
| TypeScript | const x = 1 |
let x = 1 |
const 的 '不可变' 是浅层的
const 只阻止重新赋值(rebinding), 不阻止修改对象内部属性:
|
这和 Rust 不同. Rust 的 let 不可变是深层的, 结构体内部字段也不能改. TS 要实现深层不可变需要 as const 或 Readonly<T>(后续篇章讲).
let vs var: 作用域差异
let 是块作用域(和 Go/Rust 一样), var 是函数作用域:
|
let = 房间门卡, 只能进声明所在的那间房.
var = 楼层通行证, 声明在哪间房都无所谓, 整层楼都能用.
变量提升(Hoisting)
var 的另一个坑: 声明会被"提升"到函数顶部, 但赋值不会:
|
在 Go 和 Rust 中, 使用未声明的变量直接编译失败. var 的提升行为是 JS 历史包袱, 容易导致隐蔽 bug. 这就是为什么 TS 代码永远不用 var.
规则: const > let > var
- 默认用
const - 确实需要修改值时用
let - 永远不用
var
开启 strict 模式后, ESLint 的 no-var 规则会自动禁止 var.
基础类型
number
TS 的 number 统一表示所有数字, 不区分整数和浮点:
|
| 语言 | 整数 | 浮点 |
|---|---|---|
| Go | int, int8, int16, int32, int64 |
float32, float64 |
| Rust | i8 ~ i128, u8 ~ u128 |
f32, f64 |
| TypeScript | 都是 number |
都是 number |
底层是 IEEE 754 双精度浮点(64 位), 安全整数范围: -(2^53 - 1) 到 2^53 - 1. 超出范围用 bigint.
string
|
模板字符串(反引号)类似 Go 的 fmt.Sprintf("Hi, %s", name) 或 Rust 的 format!("Hi, {name}"), 但语法更简洁, 直接在字符串内用 ${} 嵌入表达式.
boolean
|
null 和 undefined
TS 有两个表示"空"的值, 这是 JS 历史遗留:
| 值 | 含义 | 对标 |
|---|---|---|
undefined |
变量已声明但未赋值, 或函数无返回值 | Go 的零值概念 |
null |
显式表示"没有值" | Go 的 nil / Rust 的 None |
|
strictNullChecks
开启 strict 后, null 和 undefined 不能随意赋给其他类型:
|
这类似 Rust 的 Option<T>: 要表示"可能为空", 必须在类型上显式声明.
bigint
超出 number 安全范围时使用:
|
symbol
用于创建唯一标识符, 主要在框架底层使用, 日常业务代码很少直接用:
|
类型注解
类型标注写在变量名后面, 用冒号分隔:
|
类型推断
大多数时候不需要手写类型, 编译器能从赋值推断:
|
这和 Go 的 := 是一个思路:
|
何时必须显式注解
三种情况需要手写类型:
|
推荐: 能省则省
不要写 let x: number = 1 这种冗余注解. 赋值已经告诉编译器类型了.
只在推断不出或推断不准确时才写. 这和 Rust 社区的习惯一致.
数组与元组
数组
两种等价写法:
|
社区惯例用 number[] 写法, Array<number> 用于泛型嵌套时可读性更好(如 Array<[string, number]>).
元组
固定长度, 每个位置类型可以不同:
|
对比 Rust 的元组:
|
语法几乎对称: Rust 用圆括号 (), TS 用方括号 [].
元组的越界访问
TS 会检查元组的索引越界:
|
特殊类型
any: 逃生舱
any 关闭该变量的所有类型检查, 等于回到 JS 模式:
|
unknown: 安全版 any
unknown 也能接受任何值, 但使用前必须先收窄类型:
|
| 对比 | any |
unknown |
|---|---|---|
| 赋值给它 | 任何值都行 | 任何值都行 |
| 从它读取 | 随便用, 不检查 | 必须先收窄类型 |
| 类比 | Go 的 interface{} 不加断言直接用 |
Go 的 interface{} 必须 type assert 后用 |
| 安全性 | 危险 | 安全 |
规则: 需要 any 时先考虑 unknown
外部输入(API 返回值, JSON 解析结果)用 unknown, 然后通过类型守卫收窄.
any 只用于快速原型或者和无类型的第三方库交互, 是最后手段.
void 和 never
|
never 在 Rust 中对应 !(never type), 在 Go 中没有直接对应, 因为 Go 的 panic 不体现在函数签名上.
类型断言
当我们比编译器更清楚一个值的类型时, 用 as 手动指定:
|
对比其他语言:
|
|
什么时候用, 什么时候不该用
|
类型断言不是类型转换
as 不做任何运行时转换, 它只是告诉编译器"信我". 如果断言错误, 运行时照样崩:
|
这和 Go 的 x.(string) 不同, Go 在运行时会 panic 或返回 ok=false. TS 的 as 零运行时开销, 也零运行时保护.
字面量类型与 const 断言
字面量类型
变量的类型可以是一个具体的值, 而不只是 string 或 number:
|
这是 TS 类型系统的独特能力, Go 和 Rust 中没有直接对应(Rust 有 const generics, 但用途不同).
const 断言
const 声明的变量, 编译器自动推断为字面量类型:
|
对于对象和数组, 用 as const 把所有字段递归锁定为字面量类型:
|
as const 的典型用途
定义枚举值集合或配置常量时, as const 让类型系统知道精确的值:
|
比 enum 更轻量, 也是社区更推荐的做法.
快速回顾
- const/let: 默认
const, 需要改值用let, 永远不用var - 基础类型:
number(不分整数浮点),string,boolean,null,undefined - 类型注解: 写在变量名后
name: type, 能推断时省略 - 数组/元组:
number[]是数组,[string, number]是定长元组 - any vs unknown:
any放弃检查,unknown强制收窄后才能用 - 类型断言:
as Type告诉编译器"我确定", 但没有运行时保护 - 字面量类型: 类型可以是具体的值,
as const把对象所有字段锁为只读字面量
动手练习
- const vs let: 声明 5 个变量, 判断哪些该用
const, 哪些该用let, 解释理由. - 类型推断: 写
const x = [1, "hello", true], 把鼠标悬停在x上看 IDE 推断出什么类型. 然后改成元组类型标注. - unknown 练习: 写一个函数接收
unknown参数, 用typeof判断后分别处理 string/number/其他情况. - 断言对比: 分别用类型断言和类型守卫两种方式处理一个
unknown值, 体会安全性差异. - as const: 定义一个 HTTP 方法常量数组
["GET", "POST", "PUT", "DELETE"] as const, 然后用typeof提取出联合类型. - 找 bug: 下面的代码有什么问题?
const config = { port: 3000, host: "localhost" }; config.port = "8080";