01-内置数据类型与工具类型
内置数据类型与工具类型
Go 开发者已知
在 Go 中,类型系统在编译期是严格的,但运行时可以通过 reflect 包来检查和操作任意类型。Go 的基础类型包括:
// Go 基础类型
var i int // int, int8, int16, int32, int64
var u uint // uint, uint8, uint16, uint32, uint64
var f float64 // float32, float64
var b bool // bool
var s string // string
var by byte // byte (uint8 别名)
var r rune // rune (int32 别名,代表 Unicode 码点)
// 复合类型
var arr [3]int // 数组(定长)
var sli []int // 切片(变长)
var m map[string]int // 映射
var st struct{ Name string } // 结构体
var fn func(int) bool // 函数
var iface interface{} // 接口(空接口)Go 的 reflect 包可以在运行时获取类型信息和值操作:
import "reflect"
func Inspect(v any) {
t := reflect.TypeOf(v) // 获取静态类型信息
vv := reflect.ValueOf(v) // 获取运行时值信息
fmt.Println(t.Kind()) // reflect.Int, reflect.String 等
fmt.Println(t.Name()) // 类型名称
fmt.Println(vv.Kind()) // 值的种类
// 遍历结构体字段
if t.Kind() == reflect.Struct {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: %s, Type: %s\n", field.Name, field.Type)
}
}
}TS 怎么做
TypeScript 的类型系统是结构类型系统(Structural Type System),且类型仅在编译期存在,运行时会被擦除。JS 的运行时类型信息非常有限。
typeof 操作符
// TS 中的 typeof —— 编译期与运行时含义不同
const str = "hello";
type T = typeof str; // 编译期: type T = string
// 运行时 typeof —— 局限很大
console.log(typeof "hello"); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" —— 著名的 JS 历史遗留 bug
console.log(typeof []); // "object" —— 无法区分数组与普通对象
console.log(typeof {}); // "object"
console.log(typeof new Date()); // "object"
console.log(typeof (() => {})); // "function"typeof 的局限
typeof null === "object" 是 JavaScript 自诞生以来的一个 bug,但由于已有大量代码依赖此行为,至今无法修复。同时,typeof 无法区分数组、普通对象、Date 等——它们都返回 "object"。
Object.keys / values / entries
// 对象反射操作
interface User {
id: number;
name: string;
email: string;
}
const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
// 获取键数组(注意:返回 string[] 而非 (keyof User)[])
const keys = Object.keys(user);
// ^? string[]
// 获取值数组
const values = Object.values(user);
// ^? unknown[]
// 获取键值对数组
const entries = Object.entries(user);
// ^? [string, unknown][]类型安全缺陷
Object.keys() 返回 string[] 而非 (keyof T)[],这是 TS 团队的有意设计——因为对象在运行时可能包含原型链上继承的额外属性。如果确信对象纯净,可以自己封装:
function typedKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}Object.assign / freeze
// Object.assign —— 合并对象
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const merged = Object.assign(target, source);
// target 被原地修改: { a: 1, b: 3, c: 4 }
// merged === target (引用相同)
// 推荐用展开运算符替代
const merged2 = { ...target, ...source };
// Object.freeze —— 冻结对象(浅冻结)
const frozen = Object.freeze({ x: 1, y: { z: 2 } });
frozen.x = 42; // 严格模式下报错
frozen.y.z = 99; // 可以修改!嵌套对象未冻结内置工具类型 —— 分类详解
TypeScript 内置了大量工具类型,按功能分类如下:
1. 属性修饰工具类型
// Partial<T> —— 所有属性变为可选
interface Config { url: string; port: number; timeout: number; }
type PartialConfig = Partial<Config>;
// { url?: string; port?: number; timeout?: number; }
// Required<T> —— 所有属性变为必选
type RequiredConfig = Required<PartialConfig>;
// Readonly<T> —— 所有属性变为只读
type ReadonlyConfig = Readonly<Config>;
// 实现原理
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };2. 联合类型工具类型
// Exclude<T, U> —— 从 T 中排除 U 中的类型
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<string | number | boolean, string | number>; // boolean
// Extract<T, U> —— 提取 T 中属于 U 的类型
type T2 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
// NonNullable<T> —— 排除 null 和 undefined
type T3 = NonNullable<string | null | undefined>; // string
// Record<K, T> —— 构造对象类型
type PageInfo = Record<string, { title: string; url: string }>;3. 函数与返回值工具类型
// ReturnType<T> —— 获取函数返回值类型
function greet(name: string): string { return `Hello ${name}`; }
type GreetReturn = ReturnType<typeof greet>; // string
// Parameters<T> —— 获取函数参数类型元组
type GreetParams = Parameters<typeof greet>; // [name: string]
// ConstructorParameters<T> —— 获取构造函数参数类型
class Foo { constructor(a: number, b: string) {} }
type FooParams = ConstructorParameters<typeof Foo>; // [a: number, b: string]
// InstanceType<T> —— 获取实例类型
type FooInstance = InstanceType<typeof Foo>;4. 字符串字面量工具类型(TS 4.1+)
// Uppercase/Lowercase/Capitalize/Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"
// 实用场景:统一 API 响应中的键名
type ApiResponse = {
user_name: string;
user_email: string;
};
type CamelCaseKeys = {
[K in keyof ApiResponse as CamelCase<K & string>]: ApiResponse[K];
};
// 需自行实现 CamelCase 工具类型5. Pick / Omit
// Pick<T, K> —— 选取属性
interface Person { name: string; age: number; email: string; }
type NameAndAge = Pick<Person, "name" | "age">;
// { name: string; age: number; }
// Omit<T, K> —— 排除属性
type WithoutEmail = Omit<Person, "email">;
// { name: string; age: number; }
// 实现原理
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;差异分析
| 维度 | Go | TypeScript |
|---|---|---|
| 类型存在时间 | 编译期 + 运行时 | 仅编译期,运行时擦除 |
| 运行时类型检查 | reflect.TypeOf / reflect.ValueOf | typeof(局限)+ instanceof(有限) |
| 类型反射能力 | 完整 — 字段遍历、方法调用、值修改 | 无反射能力,需借助 TS 编译器 API |
| 结构体字段操作 | reflect.StructField 完整信息 | Object.keys() 返回 string[] |
| 对象冻结 | 无内置,需用 sync/atomic 或自定义 | Object.freeze() 内置(浅冻结) |
| 对象合并 | 手写循环或使用第三方库 | Object.assign() / 展开运算符 |
Bad Practice
1. 滥用 as 断言绕过类型系统
// Bad: 用 as any 绕过类型检查
const data = JSON.parse(response) as any;
console.log(data.user.name.toUpperCase()); // 运行时可能崩溃
// 也不要用双重断言
const num = (42 as unknown) as string; // 完全错误的类型2. 过度依赖 typeof 进行运行时判断
// Bad: typeof 无法区分复杂类型
function process(value: unknown) {
if (typeof value === "object") {
// 这里 value 可能是: null、数组、普通对象、Date...
value.foo; // TS 报错:Object is possibly null
}
}3. 修改 frozen 对象而不检查
// Bad: 尝试修改 frozen 对象
const settings = Object.freeze({ theme: "dark", lang: "zh" });
settings.theme = "light"; // 静默失败(非严格模式)或报错(严格模式)Best Practice
1. 自定义类型安全的对象操作函数
// Best: 类型安全的 keys 封装
function keys<T extends Record<string, unknown>>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
// 类型安全的 entries
function entries<T extends Record<string, unknown>>(obj: T): [keyof T, T[keyof T]][] {
return Object.entries(obj) as [keyof T, T[keyof T]][];
}
interface User {
id: number;
name: string;
}
const user: User = { id: 1, name: "Alice" };
for (const key of keys(user)) {
console.log(key); // key 类型为 "id" | "name"
}2. 深度冻结工具函数
// Best: 深度冻结
function deepFreeze<T extends Record<string, any>>(obj: T): Readonly<T> {
const propNames = Object.getOwnPropertyNames(obj);
for (const name of propNames) {
const value = obj[name];
if (value && typeof value === "object") {
deepFreeze(value);
}
}
return Object.freeze(obj);
}
const config = deepFreeze({ nested: { value: 42 } });
config.nested.value = 0; // 严格模式下报错3. 使用类型守卫替代 typeof
// Best: 使用类型守卫
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function process(value: unknown) {
if (isRecord(value)) {
value.foo; // 类型安全
}
}4. 工具类型组合使用
// Best: 组合工具类型表达精确约束
interface ServerConfig {
host: string;
port: number;
tls: boolean;
cert?: string;
key?: string;
}
// 只读且必填的配置类型(排除可选属性)
type StrictServerConfig = Readonly<Required<Pick<ServerConfig, "host" | "port" | "tls">>>;核心建议
TypeScript 的工具类型是编译期类型转换的强大工具,配合类型守卫、泛型约束可以编写高度类型安全的代码。记住:TS 类型是编译期幻影,运行时你需要 zod、io-ts 等方案做真正的数据校验(详见第8篇)。
附录:Null Safety 操作符与链式语法糖
TypeScript(以及它所 target 的现代 JavaScript)提供了几个专门用来安全处理 null / undefined 的操作符。这些是 Go 开发者从零学 TS 时最直观、最常用的语法糖。
1. 可选链(Optional Chaining)?.
作用:在访问深层嵌套属性时,如果中间某一级是 null 或 undefined,表达式短路返回 undefined,而不是抛 TypeError。
interface User {
name: string
address?: {
city: string
zip?: string
}
getProfile?(): string
}
const user: User = { name: 'Alice' }
// 没有可选链的时候 —— 每一层都要手动判断
const city = user.address
? user.address.city
: undefined
// 有可选链 —— 一行搞定
const city2 = user.address?.city // undefined(不会崩)
const zip = user.address?.zip // undefined
const profile = user.getProfile?.() // undefined(方法调用也能用可选链!)可选链的三种形式
// 1. 属性访问
obj?.prop
// 2. 计算属性访问
obj?.[expr]
// 3. 方法调用
func?.(args)与 Go 的对比
// Go —— 没有可选链,每个字段都要手动判 nil
type Address struct {
City string
}
type User struct {
Name string
Address *Address
}
// 取 city
var city string
if u.Address != nil {
city = u.Address.City
}
// TS 等价
// const city = user.address?.cityGo 开发者应该在 Go 1.22+ 的 if v := get(); v != nil { ... } 模式中有所体会——但 TS 的可选链更加简洁,而且可以链式连续使用多层。
2. 空值合并(Nullish Coalescing)??
作用:当左侧表达式为 null 或 undefined 时,返回右侧默认值;否则返回左侧值。
const input1: string | undefined = undefined
const input2: string | null = null
const input3: string = 'hello'
const result1 = input1 ?? 'default' // 'default' —— undefined 触发了默认值
const result2 = input2 ?? 'default' // 'default' —— null 触发了默认值
const result3 = input3 ?? 'default' // 'hello' —— 有值就用值?? vs || —— 关键区别
const count = 0
const name = ''
const isEnabled = false
// `||` 看的是"真值"(falsy value)
const a = count || 100 // 100 —— 0 是 falsy!
const b = name || 'default' // 'default' —— '' 是 falsy!
const c = isEnabled || true // true —— false 是 falsy!
// `??` 只看 null / undefined
const a2 = count ?? 100 // 0 —— 0 不是 null/undefined,保留原值
const b2 = name ?? 'default' // '' —— 空字符串不是 null/undefined
const c2 = isEnabled ?? true // false —— false 不是 null/undefined|| 把 0、''、false 都当成"空";?? 只把 null 和 undefined 当成"空"。在 Go 中你只有 nil 一个空值概念,但在 JS/TS 中 0、''、false 都是有效值,所以绝大多数场景应该用 ?? 而不是 ||。
与 Go 的对比
// Go —— 无对应的 `??` 操作符
// 可以用 if 判断
input := getString()
result := "default"
if input != "" {
result = input
}
// 或者用 helper 函数
func defaultIfEmpty(s, d string) string {
if s == "" { return d }
return s
}注意 Go 没有区分"空字符串"和"未设置"——string 的零值就是 ""。这是与 TS 设计哲学的重要差异。
3. 空值合并赋值(Nullish Coalescing Assignment)??=
作用:仅在变量当前值为 null 或 undefined 时,才赋予新值。是 x ??= y 等价于 x ?? (x = y) 的语法糖。
let config: { port?: number; host?: string } = {}
// 当 config.port 为 null/undefined 时才赋值
// 等价于:if (config.port === null || config.port === undefined) { config.port = 3000 }
config.port ??= 3000 // config.port 是 undefined → 赋值为 3000
config.port ??= 8080 // config.port 现在是 3000 → 不覆盖
let count: number | null = null
count ??= 0 // count 是 null → 赋值为 0
// count ??= 5 // count 现在是 0 → 0 不是 null/undefined,不赋值// Go 无对应操作符
var port *int
if port == nil {
p := 3000
port = &p
}4. 逻辑与赋值 / 逻辑或赋值(&&= / ||=)
// &&= —— 仅在左侧为真值时赋值
let x = 1
x &&= 2 // x 是 1(真值)→ x = 2
let y = 0
y &&= 2 // y 是 0(假值)→ 不赋值,y 仍为 0
// ||= —— 仅在左侧为假值时赋值
let a = 0
a ||= 42 // a 是 0(假值)→ a = 42
let b = 1
b ||= 42 // b 是 1(真值)→ 不赋值,b 仍为 1与 Go 的对比
Go 没有这些赋值语法糖,通常用 if 语句实现相同逻辑:
// Go 写法
if x != "" {
// 类似 &&= 的效果
}
if defaultValue != "" {
// 类似 ||= 的效果
}但 Go 开发者写 TS 时最容易犯错的是混淆 ?? 和 ||——因为 Go 只有一个零值概念,而 JS 有 6 个 falsy 值(false、0、''、null、undefined、NaN)。
5. 非空断言(Non-null Assertion)!
作用:告诉 TypeScript 编译器"这个值一定不是 null 也不是 undefined",跳过编译期空值检查。注意这没有运行时效果,只是类型层面告诉编译器"相信我"。
// 场景:你已经人工确认某个值不为 null
function processElement(id: string) {
const el = document.getElementById(id) // 返回 HTMLElement | null
el!.textContent = 'Hello' // 你确信这个 id 存在 → 用 ! 断言
}
// 另一种常见用法:类属性延迟初始化
class App {
private config!: Config // 表示"虽未在 constructor 中初始化,但使用时一定已有值"
init() { this.config = loadConfig() }
start() { console.log(this.config.port) } // 不使用 ! 会报错
}非空断言的风险
! 只影响类型检查,不影响运行时。如果在运行时值真的是 null,仍然会抛 TypeError:
const el = document.getElementById('non-existent')
el!.textContent = 'Hello' // ✅ 编译通过
// ❌ 运行时: Cannot read properties of null非空断言是最后的手段。能用类型守卫或可选链解决时,不要用 !:
// ❌ Bad:用非空断言逃避检查
function getFirst(arr: number[]) {
return arr[0]! // arr 为空数组时返回 undefined!断言错误
}
// ✅ Best:用类型守卫或可选链
function getFirst(arr: number[]): number | undefined {
return arr[0] // 返回类型本身就允许 undefined
}
// ✅ Best:用 ?? 提供默认值
function getFirst(arr: number[], fallback: number): number {
return arr[0] ?? fallback
}6. 总和对比
| 操作符 | 名称 | 作用 | Go 对标 |
|---|---|---|---|
?. | 可选链 | 安全访问深层属性,遇 null/undefined 短路 | 手动判 nil |
?? | 空值合并 | 左侧为 null/undefined 时取右侧默认值 | if x != nil { ... } |
??= | 空值合并赋值 | 仅在 null/undefined 时赋值 | if x == nil { x = v } |
&&= | 逻辑与赋值 | 左侧为真值时赋值 | if x { x = v } |
||= | 逻辑或赋值 | 左侧为假值时赋值 | if !x { x = v } |
! | 非空断言 | 编译期跳过 null 检查,无运行时效果 | 无对应(类型断言) |
Go 开发者上手建议
在 TS 中处理可选值/空值的优先顺序:
- 首选:类型系统层面用
| undefined或| null明确标注 - 选型:访问属性用
?.,取默认值用?? - 初始化:用
??=惰性赋值 - 最后手段:确认 100% 非空时用
!(应尽量少用) - 永远不要:用
||替代??来处理 null/undefined(会吃掉0/''/false)