01-基础类型与变量声明
基础类型与变量声明
概述
本文面向有 Go 经验的开发者,系统介绍 TypeScript 的基础类型系统和变量声明方式。Go 与 TypeScript 都是静态类型语言,但二者的类型哲学存在显著差异:Go 偏向隐式结构化类型(Structural Typing),TypeScript 则是显式结构化类型,并且在类型表达力上更接近函数式语言。
警示:前方高能 — TypeScript 类型体操
别被下面的代码吓到。TypeScript 的类型系统是图灵完备的,意味着你可以在编译期用类型完成任意计算。以下展示几个极端案例:
// 从函数签名提取参数类型
type MyParams<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never
// 深层递归只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Function | string | number | boolean
? T[K]
: DeepReadonly<T[K]>
}
// 模板字面量递归(TS 4.1+)
type ReverseStr<S extends string> =
S extends `${infer F}${infer R}` ? `${ReverseStr<R>}${F}` : ''
// 判别联合类型
type IsUnion<T, U = T> = T extends U ? [U] extends [T] ? false : true : never以上全部在编译期完成,运行时代码零开销。熟练掌握这些技巧需要时间,别焦虑——绝大多数的日常场景下,简单的类型注解就足够了。
Go 开发者已知
在 Go 中,基础类型系统较为简洁:
// 基础类型
var a int = 42
var b float64 = 3.14
var c string = "hello"
var d bool = true
// 短变量声明(函数内)
e := "inferred"
// 零值初始化
var f int // 0
var g string // ""
var h bool // false
// 空接口(类似 any)
var i interface{}
i = 42
i = "string"
i = struct{}{}Go 的类型系统核心特点:
- 强类型,但不支持联合类型
interface{}可接受任意类型,但丢失了类型信息- 没有
null的概念(指针为nil) - 不支持字面量类型
TypeScript 怎么做
基础类型注解
// 基础类型
let a: number = 42
let b: string = 'hello'
let c: boolean = true
// 类型推断(推荐)
let d = 42 // 推断为 number
let e = 'hello' // 推断为 string
// null 与 undefined
let f: null = null
let g: undefined = undefined
// bigint
let h: bigint = 9007199254740991n
// symbol
let i: symbol = Symbol('unique')联合类型
// 联合类型:Go 无法直接表达
let id: string | number
id = 'abc123' // OK
id = 42 // OK
// id = true // Error
// 字面量联合类型
type Status = 'active' | 'inactive' | 'pending'
let status: Status = 'active'any vs unknown vs never vs void
// any —— 逃逸类型检查(应避免)
let x: any = 42
x = 'string' // OK
x.toUpperCase() // OK,但运行时可能炸
// unknown —— 安全的 any
let y: unknown = 42
y = 'string' // OK
// y.toUpperCase() // Error: Object is of type 'unknown'
if (typeof y === 'string') {
y.toUpperCase() // OK,类型收窄后
}
// void —— 没有返回值
function log(msg: string): void {
console.log(msg)
}
// never —— 永远不会返回
function throwError(msg: string): never {
throw new Error(msg)
}
// 穷举检查
type Shape = 'circle' | 'square'
function area(shape: Shape): number {
switch (shape) {
case 'circle': return 1
case 'square': return 2
default: {
// shape 被收窄为 never
const _exhaustive: never = shape
return _exhaustive
}
}
}const / let / var
const MAX_SIZE = 100 // 常量,不可重新赋值
let count = 0 // 可变变量,块级作用域
var name = 'old' // 函数级作用域(不推荐)避免使用 var
var 存在变量提升(hoisting)和函数级作用域问题,在现代 TypeScript 中应始终使用 const 或 let。
// var 的怪异行为
console.log(foo) // undefined(不会报错!)
var foo = 1而 let 和 const 存在暂时性死区(TDZ),在声明前访问会抛出错误。
const 声明能否手动加类型注解?
const MAX: number = 100 // 语法允许,但一般不这么写
const MIN = 50 // 更推荐的写法语法上完全合法。但绝大多数场景下不推荐对 const 加类型注解,原因有二:
1. 丢失精确性:const 声明的值不可变,TS 会自动推断为最窄的字面量类型;加上注解反而拓宽为宽泛类型:
const MIN = 50 // 类型:50(字面量类型)
const MIN_N: number = 50 // 类型:number(丢失了精确值信息)
// 实际影响:
// MIN 可赋值给更窄的类型联合:type Small = 10 | 20 | 50 → 合法
// MIN_N 只能赋值给 number,不能赋值给 Small2. 冗余啰嗦:值就在右边写着,类型显而易见,加注解纯属画蛇添足。
| 声明方式 | 是否可加注解 | 推断结果 | 推荐 |
|---|---|---|---|
const x = 42 | 可选省略 | 42(字面量) | ✅ 默认首选 |
const x: number = 42 | 可写 | number(拓宽) | ❌ |
let x = 42 | 可选省略 | number(自动拓宽) | ✅ |
let x: number = 42 | 可写 | number | ✅ 复杂场景用 |
差异分析
| 维度 | Go | TypeScript |
|---|---|---|
| 类型推断 | 有限,依赖 := | 强劲,几乎所有场景可推断 |
| 联合类型 | 不支持 | 一等公民 string | number |
| 空值 | nil 与零值 | null / undefined 分离 |
| 字面量类型 | 不支持 | type Day = 1 | 2 | 3 |
| 空接口 vs any | interface{} 需类型断言 | any 彻底逃逸检查 |
| 零值 | 自动零值初始化 | 无零值,必须显式赋值 |
结构化类型(Structural Typing)
Go 和 TypeScript 都采用结构化类型系统(鸭子类型),但 Go 的结构化发生在隐式实现接口时,而 TypeScript 的结构化发生在编译期类型兼容性检查中。
// TypeScript 结构化类型
interface Point {
x: number
y: number
}
const p = { x: 1, y: 2 } // 自动兼容 Point
const q = { x: 1, y: 2, z: 3 } // 额外属性也兼容Bad Practice
// Bad: 滥用 any 逃逸类型安全
function process(data: any) {
return data.name.toUpperCase() // 运行时可能崩溃
}
// Bad: 使用空对象类型 {} 而非 Record
function validate(obj: {}) {} // 可接收任何非 null/undefined 值
// Bad: 忽略 strictNullChecks
// tsconfig.json 中未开启 strictNullChecks
function getName(user: { name: string | null }) {
return user.name.toUpperCase() // 运行时崩溃
}
// Bad: over-annotation 过度注解
const name: string = 'hello' // 多余,TS 能推断
const items: Array<number> = [1, 2, 3] // 多余Best Practice
// Good: 优先 unknown 而非 any
function safeProcess(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
return (data as { name: string }).name.toUpperCase()
}
throw new Error('Invalid data')
}
// Good: 开启 strictNullChecks
// tsconfig.json: "strictNullChecks": true
function getName(user: { name: string | null }): string {
return user.name?.toUpperCase() ?? 'UNKNOWN'
}
// Good: 利用联合类型精确表达
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string }
// Good: 让类型推断工作
const items = [1, 2, 3] // number[]
const config = { host: 'local', port: 8080 } // { host: string; port: number }
// Good: 使用字面量类型枚举值
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
// Good: 穷举检查确保安全
type Color = 'red' | 'green' | 'blue'
function getHex(c: Color): string {
const map: Record<Color, string> = {
red: '#FF0000',
green: '#00FF00',
blue: '#0000FF',
}
return map[c]
}开启 strict 模式
在 tsconfig.json 中开启 strict: true 可获得完整的类型安全保护,包括 strictNullChecks、noImplicitAny、strictFunctionTypes 等关键检查。
{
"compilerOptions": {
"strict": true
}
}总结
TypeScript 的类型系统比 Go 更加丰富和灵活。作为 Go 开发者,你需要适应的是:
- 联合类型 —— 用类型描述值域,而非运行时检查
null/undefined分离 —— 显式声明可空性unknown优先于any—— 保持类型安全的最后防线- 放弃
var—— 拥抱const和let
下一章将介绍 接口与类型别名,这是 TypeScript 类型系统的核心构造。
附录:TypeScript 全部类型一览
| 类别 | 类型 | TS 示例 | Go 对应 / 说明 |
|---|---|---|---|
| 基本类型 | number | let x: number = 42 | int / float64,TS 统一为浮点数 |
string | let s: string = "hello" | string | |
boolean | let b: boolean = true | bool | |
bigint | let b: bigint = 100n | int64 / big.Int,任意精度整数 | |
symbol | let s: symbol = Symbol("k") | 无对应,唯一标识符 | |
null | let n: null = null | nil(但 Go 是值而非类型) | |
undefined | let u: undefined = undefined | 无对应,表示未初始化 | |
void | function f(): void {} | 函数无返回值 | |
never | function e(): never { throw ... } | 无对应,不可达类型 | |
| 对象类型 | object | let o: object = {} | interface{},非原始值 |
Object | let o: Object = {} | 所有类型的超类型(慎用) | |
{} | let o: {} = "hi" | 空对象类型,可接受除 null/undefined 外任何值 | |
| 数组类型 | Array<T> | let arr: number[] = [1,2] | []int |
readonly T[] | let r: readonly number[] = [1,2] | 无直接对应,不可变数组 | |
元组 [T, U] | let t: [string, number] | 数组,但 TS 可约束长度和元素类型 | |
| 特殊类型 | any | let a: any = 42 | interface{},关闭类型检查 |
unknown | let u: unknown = 42 | interface{},安全的 any | |
| 复合类型 | 联合 A | B | let id: string | number | 无直接对应(Go 1.18 无联合类型) |
交叉 A & B | type C = A & B | 无对应(Go 结构体嵌入类似但不同) | |
| 字面量类型 | type S = 'a' | 'b' | 无对应 | |
枚举 enum | enum Color { Red, Green } | const + iota 模式 | |
| 函数类型 | type F = (a: number) => string | func(int) string | |
| 接口与类 | interface | interface User { name: string } | interface / struct |
type 别名 | type ID = string | number | 无对应(type alias 不同) | |
class | class Animal {} | struct + 方法 | |
abstract class | abstract class Animal {} | interface(Go 无抽象类) |
区分 number 和 bigint
const n: number = 9007199254740991 // Number.MAX_SAFE_INTEGER
const b: bigint = 9007199254740992n // 可表示更大整数
// number 用于大多数场景
// bigint 用于需要超大整数或精确整数运算的场景never 的妙用
never 是 TypeScript 类型系统中"底类型"(bottom type),是所有类型的子类型。在穷举检查中非常有用:
type Shape = 'circle' | 'square'
function area(s: Shape): number {
switch (s) {
case 'circle': return Math.PI
case 'square': return 1
default:
// 如果未来有人给 Shape 新增 'triangle',此处编译报错
const _exhaustive: never = s
return _exhaustive
}QA 附录
为什么笔记中 const 示例都不写类型注解?
因为这是 TypeScript 的约定俗成的最佳实践。
核心原因已在正文中说明:const 推断为最窄的字面量类型,加注解反而拓宽类型、丢失精度。更深层的理由:
值即类型:
const x = 'hello'——值'hello'本身就是最精确的类型描述,加: string是信息退化。统一风格:整个 TypeScript 社区的代码规范(包括 TS 官方团队、React、Vue、Angular 等主流框架)都遵循此模式。以下 TS 官方源码摘录:
// TypeScript 编译器自身源码中的写法 const EmptyArray = [] // never[] const objectType = 'Object' // 'Object' const DEFAULT_MAX_LENGTH = 100 // 100重构友好:如果你加上了
const val: SomeType = expr,未来expr的类型被重构后,注解可能变成错误或误导信息;去掉注解后 TS 自动推断,重构时类型永远正确。
| 直觉 | 实际 |
|---|---|
| "显式类型更安全" | 对 const 而言,隐式字面量类型更精确 |
| "Go 中显式类型是常态" | TS 的推断能力远超 Go,两者习惯不同 |
| "const 不加注解会不会不清晰?" | 值就在等号右边,IDE hover 即见类型 |
TS 与 JS 在类型上的恩爱情仇
TypeScript 和 JavaScript 的类型关系,可以浓缩为一句话:
JS 负责运行时的值,TS 负责编译期的型。
核心矛盾
| 维度 | JavaScript | TypeScript |
|---|---|---|
| 时机 | 运行时 | 编译时(设计时) |
| 性质 | 动态、可变的类型标签 | 静态、固定的类型约束 |
| 载体 | 值本身(typeof 可检查) | 源代码中的注解(编译后消失) |
| 强制力 | 无——任何操作都可能在运行时炸 | 强——类型不匹配直接编译报错 |
| 图灵完备 | 运行时——JS 本身是图灵完备语言 | 编译期——类型系统也是图灵完备的 |
恩怨史
- JS 诞生(1995):Brendan Eich 用 10 天设计的动态语言,类型?不存在的。
- ES3/ES5 时代:
typeof、instanceof是唯一的类型检查手段。大型项目靠人为纪律(匈牙利命名法等)维持类型安全。 - Google Closure Compiler(2009):首次尝试用 JSDoc 注释给 JS 加类型——注解写注释里,工具提取后做静态分析。但只能当玩具用。
- TypeScript 诞生(2012):Anders Hejlsberg 决定另起炉灶——类型注解不再藏于注释,而是一等语言特性,同时保证对 JS 的反向兼容(任何 JS 都是合法 TS)。
- TS 爆发(2016-2020):Angular 2(2016)+ VS Code 带动 TS 普及。TS 从一个"可选注释"演变为"JS 的事实标准超集"。
- 如今:JS 也在倒吸 TS 特性——TC39 提案中的类型注解语法(Type Annotations in JSDoc / Type Annotations proposal)试图让 JS 原生支持 TS 风格的注解,但这又是另一个故事了。
与 Python 2 → 3 类型系统演进的对比
# Python 2(无类型提示)— 类似没有 TS 的 JS
def greet(name):
return "Hello " + name # name 可以是任何类型
# Python 3.0-3.4(可选的 function annotations)
def greet(name: str) -> str: # 语法合法,但解释器完全忽略
return "Hello " + name
# Python 3.5+(正式引入 typing 模块,渐进类型系统)
from typing import Optional, Union
def greet(name: Optional[str] = None) -> str:
return "Hello " + (name or "World")对比分析:
| 维度 | TS ↔ JS | Python typing ↔ Python |
|---|---|---|
| 类型语法 | 独立语言特性 | 标准库模块 + 注解语法 |
| 类型擦除 | 编译期(tsc → .js)全部移除 | 运行时保留,但解释器不检查 |
| 检查工具 | tsc 内置,开箱即用 | mypy / pyright 等第三方工具 |
| 社区普及度 | 事实标准,99% 项目使用 | 广泛但不强制,Flask/Django 官方仍以动态为主 |
| 强制力度 | strict: true 下几乎全量检查 | 全靠开发者自觉跑 mypy |
| 动态兜底 | any 可绕过一切检查 | Any 等价,且 # type: ignore 单行绕过 |
| 渐进迁移 | .js → .ts 一步步改,allowJs 允许混用 | .py 原地加注解,无需重命名文件 |
根本差异在于哲学:
- TS 的立场:类型是语言的一部分,编译器是第一道防线。你不写类型是偷懒,不是风格。
- Python typing 的立场:类型是可选的文档,提升可读性,语言本身不关心。你写了是好习惯,不写也完全 OK。
- JS(ECMAScript)的立场:类型是动态的,解释器只关心值。TC39 正在讨论"类型注解作为注释"的阶段 1 提案,但在可见的未来,JS 运行时仍不会关心类型。
给 Go 开发者的视角转换:
Go 是"静态类型从头到尾"——类型参与编译生成机器码,运行时类型信息(reflect)可查询但不可变。TS 是"静态类型只在编译期"——编译后的 JS 是纯动态代码,number、string、boolean 等类型信息荡然无存。这是两种截然不同的"静态类型"概念,请务牢记。
附录:如何理解类型擦除
Important:什么是类型擦除?
类型擦除(Type Erasure)是 TypeScript 最核心的设计决策:所有类型注解在编译为 JavaScript 时被完全移除,运行时中不存在任何 TS 类型信息。
// TypeScript 源码
function greet(name: string): string {
return `Hello, ${name.toUpperCase()}`
}
const result: string = greet('world')↓ 编译后(JavaScript):
// 所有 :string 等类型注解全部消失
function greet(name) {
return `Hello, ${name.toUpperCase()}`
}
const result = greet('world')类型擦除的具体表现
| 场景 | TS 代码 | 编译后 JS | 说明 |
|---|---|---|---|
| 变量注解 | let x: number = 42 | let x = 42 | 类型注解被移除 |
| 函数参数/返回 | function f(a: number): string | function f(a) | 参数和返回类型被移除 |
| 接口 | interface User { name: string } | 完全消失 | 接口是纯编译期结构 |
| 类型别名 | type ID = string | number | 完全消失 | 别名在运行时无痕迹 |
| 泛型 | function id<T>(x: T): T { return x } | function id(x) { return x } | 类型参数全部擦除 |
| 枚举 | enum Color { Red } | var Color = { Red: 0 } | 枚举会编译为 JS 对象(反向映射) |
| 装饰器(emitDecoratorMetadata) | @log | __decorate([log], ...) | 元数据保留但类型信息擦除 |
类型擦除的影响
正面影响:
- 零运行时开销:TS 类型系统是免费的——编译期越复杂,生成代码越简洁
- 无缝 JS 生态:编译后的 JS 就是标准 JS,可在任何 JS 运行环境执行
- 互操作性:任何 JS 库无需修改即可在 TS 中使用(声明文件
.d.ts单独提供类型)
负面影响:
- 无法在运行时获取类型:
typeof/instanceof无法区分 TS 层面的类型// 编译后全是 string,无法区分 type UserID = string type OrderID = string function process(id: UserID | OrderID) { // 运行时无法知道传入的是 UserID 还是 OrderID } - 类型防御:需要运行时类型检查时,必须手动编写类型守卫(Type Guards)或使用
zod/io-ts等验证库
与 Go 的关键对比
// Go 的类型在运行时仍然存在
var x int = 42
fmt.Println(reflect.TypeOf(x)) // 输出 "int" —— 运行时可查询
var y interface{} = "hello"
s := y.(string) // 运行时类型断言 —— 类型信息存活// TS 的类型在运行时全部消失
let x: number = 42
console.log(typeof x) // "number" —— 这是 JS 的 typeof,不是 TS 类型
// 运行时无法区分 TS 层面的类型别名:
// type UserID = string vs type OrderID = string一句话总结:Go 的类型是运行时一等公民,TS 的类型是编译期一次性道具。两者都是静态类型,但 Go 的类型参与程序生命周期,TS 的类型用过即弃。