阅读视图

发现新文章,点击刷新页面。

ts类型工具

TypeScript 提供了一套强大的内置工具类型(Utility Types),用于从已有类型中派生出新类型,从而提升代码的健壮性、可维护性和开发效率。这些工具类型就像“类型世界的高阶函数”,能对类型进行组合、裁剪、转换等操作。


🧰 常用 TypeScript 工具类型速查表

分类 工具类型 作用
基础修饰 Partial<T> T 的所有属性变为可选
Required<T> T 的所有属性变为必填(移除 ?
Readonly<T> T 的所有属性变为只读
结构挑选 Pick<T, K> T选取指定键 K 的属性
Omit<T, K> T剔除指定键 K 的属性
Record<K, T> 构造一个键为 K、值为 T 的对象类型
类型过滤 Exclude<T, U> 从联合类型 T排除可赋值给 U 的类型
Extract<T, U> 从联合类型 T提取可赋值给 U 的类型
NonNullable<T> T 中移除 nullundefined
函数相关 ReturnType<T> 获取函数 T返回值类型
Parameters<T> 获取函数 T参数类型元组
ConstructorParameters<T> 获取构造函数的参数类型
InstanceType<T> 获取构造函数的实例类型
ThisParameterType<T> / OmitThisParameter<T> 处理函数中的 this 参数

🔍 典型示例与应用场景

1. Partial<T>:局部更新

interface User {
  id: number;
  name: string;
  email: string;
}

// 所有字段变为可选
type UpdateUser = Partial<User>;

function updateUser(id: number, changes: UpdateUser) {
  // 只需传入要修改的字段
}
updateUser(1, { name: "Alice" }); // ✅

2. Pick<T, K> / Omit<T, K>:按需选择或排除字段

type UserPreview = Pick<User, 'id' | 'name'>; // { id: number; name: string }
type UserWithoutId = Omit<User, 'id'>;        // { name: string; email: string }

3. Record<K, T>:构建配置对象

type Theme = 'light' | 'dark';
type ColorMap = Record<Theme, string>; // { light: string; dark: string }

4. ReturnType<T>:推导函数返回类型

function fetchUser() {
  return { id: 1, name: "Bob" };
}

type User = ReturnType<typeof fetchUser>; // { id: number; name: string }

5. Exclude<T, U> / Extract<T, U>

type Status = 'loading' | 'success' | 'error';
type ValidStatus = Exclude<Status, 'loading'>; // 'success' | 'error'
type LoadingOnly = Extract<Status, 'loading'>; // 'loading'

💡 小贴士

  • 这些工具类型基于 映射类型(Mapped Types)条件类型(Conditional Types)infer 等高级特性实现。
  • 它们是不可变的:不会修改原始类型,而是生成一个新类型。
  • 组合使用,例如:
    type SafeUser = Readonly<Partial<User>>;
    

如需深入某个工具类型的源码实现或更多实战案例,可以告诉我具体类型(如 OmitReturnType),我可以进一步详解!

类属性公共还是私有

决定一个类的属性(成员变量)是公共(public)还是私有(private) ,是面向对象设计中的核心问题之一。这不仅关乎代码封装性,还直接影响系统的可维护性、可扩展性和健壮性

以下是系统化的思考框架和实用原则,帮助你做出合理决策:


🔑 核心原则:最小暴露原则(Principle of Least Exposure)

“只暴露必须暴露的内容,其余一律隐藏。”

换句话说:
默认私有,只有当确实需要外部访问时,才设为公共。


一、判断标准:问自己这几个问题

1. 外部是否需要直接读取这个值?

  • ✅ 是 → 考虑 public 或提供 getter
  • ❌ 否 → private

📌 示例:

class BankAccount {
  private balance: number; // 外部不应直接读余额(需鉴权/日志)
  getBalance(): number { /* ... */ } // 通过方法控制访问
}

2. 外部是否需要直接修改这个值?

  • ✅ 是 → 考虑 public 或提供 setter(但要谨慎!)
  • ❌ 否 → private

⚠️ 直接暴露可变状态容易导致 bug:

// 危险!
user.profile.settings.darkMode = true; // 绕过校验/事件通知

更好的方式:

user.setTheme('dark'); // 内部可触发 re-render / save / log

3. 这个属性是否属于“内部实现细节”?

  • 如果未来可能重构、重命名或删除它 → 必须私有
  • 如果它是稳定契约的一部分(如 API 返回结构)→ 可 public

💡 例子:

  • 缓存字段(private cache: Map<...>)→ 私有
  • 用户 ID(public id: string)→ 公共(业务标识)

4. 是否需要保持对象的“不变性”(Invariants)?

如果属性参与维持对象的内部一致性,则必须私有,并通过方法控制变更。

📌 示例:矩形的宽高不能为负数

class Rectangle {
  private _width: number;
  private _height: number;

  setWidth(w: number) {
    if (w < 0) throw new Error('Width must be positive');
    this._width = w;
  }
}

二、优先使用 方法(Method)而非公共属性

即使需要“读取”或“设置”,也优先提供方法而非直接暴露属性:

场景 推荐做法
读取计算值 getFullName() 而非 fullName(除非是简单数据)
设置需校验 setEmail(email) 而非 email = ...
触发副作用 activate() 而非 isActive = true

✅ 好处:

  • 未来可加日志、权限、缓存、事件通知等逻辑
  • 避免“属性被意外覆盖”导致状态不一致

三、特殊情况处理

✅ 可以公开的属性类型

类型 说明 示例
不可变数据 初始化后永不改变 public readonly id: string
纯数据载体(DTO/POJO) 仅用于传输,无行为 interface UserDTO { name: string; email: string }
配置对象 明确设计为可读写的配置 public config: RenderConfig(但建议用 getter/setter 封装)

❌ 应避免公开的属性

  • 内部状态(如 isLoading, retryCount
  • 依赖其他属性的派生值(如 fullName = firstName + lastName → 应用 getter)
  • 敏感数据(密码、token、余额)
  • 复杂对象引用(如 private domElement: HTMLElement

四、TypeScript / JavaScript 中的具体实践

方案 1:使用 # 私有字段(推荐,ES2022+)

class Timer {
  #startTime: number;
  #isRunning = false;

  start() {
    this.#startTime = Date.now();
    this.#isRunning = true;
  }

  get elapsed() {
    return this.#isRunning ? Date.now() - this.#startTime : 0;
  }
}

✅ 真正私有,运行时安全

方案 2:TypeScript private(仅开发时保护)

class Logger {
  private logs: string[] = [];
  log(msg: string) { this.logs.push(msg); }
}

⚠️ 注意:编译后仍可被外部访问,仅防“手误”

方案 3:readonly + 公共(用于不可变数据)

class Point {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}
}

✅ 安全暴露,且不可修改


五、团队协作建议

  1. 约定优于配置:团队统一规则,如“所有状态属性默认私有”
  2. 代码审查重点:检查是否有不必要的 public 属性
  3. 文档说明:对 public 属性明确其用途和约束

✅ 快速决策流程图

这个属性需要被外部访问吗?
│
├─ 否 → private / #
│
└─ 是 → 
     ├─ 是否需要修改? 
     │   ├─ 是 → 提供 setter 方法(而非直接 public)
     │   └─ 否 → 
     │        ├─ 是否不可变? → public readonly
     │        └─ 是否计算值? → 提供 getter 方法
     │
     └─ 是否属于稳定数据契约? → 可 public(如 DTO)

🎯 总结:黄金法则

“属性代表状态,状态应受控。
暴露行为(方法),而非状态(属性)。”

  • 默认 私有private#
  • 仅在必要且安全时暴露为公共
  • 优先通过 方法 控制访问,而非直接暴露字段
  • 对于纯数据对象(如 API 响应),可适当放宽

这样做,你的类将更健壮、更易测试、更易演进。

TS和JS成员变量修饰符

在 TypeScript 和 JavaScript 中,类成员变量(属性)的修饰符(Modifiers) 用于控制其可见性、可访问性和可变性。两者在能力上有显著差异:TypeScript 提供了更丰富的编译时修饰符,而 JavaScript(ES2022 起)引入了运行时私有字段

下面从 TypeScriptJavaScript 两个角度分别说明,并对比异同。


一、TypeScript 类成员变量修饰符(编译时)

TypeScript 在 编译阶段 提供以下关键字作为访问修饰符:

修饰符 含义 是否生成 JS 代码 可见性
public 公共(默认) ❌ 不生成额外代码 类内、子类、外部均可访问
private 私有 ❌ 仅类型检查,不阻止运行时访问 仅类内部可访问(TS 编译时报错)
protected 受保护 ❌ 仅类型检查 类内部 + 子类可访问
readonly 只读 ❌ 仅类型检查 初始化后不可修改(TS 报错)

✅ 示例(TypeScript):

class User {
  public name: string;        // 默认就是 public
  private id: number;         // TS 禁止外部访问
  protected email: string;    // 子类可访问
  readonly createdAt: Date;   // 初始化后不可改

  constructor(name: string, id: number, email: string) {
    this.name = name;
    this.id = id;
    this.email = email;
    this.createdAt = new Date();
  }
}

⚠️ 注意:
private / protected 只在 TypeScript 编译时生效,编译成 JS 后,这些字段仍是普通属性,运行时仍可被访问或修改

// 编译后的 JS(无 private 保护!)
const user = new User("Alice", 1, "a@example.com");
console.log(user.id); // ✅ 能访问!JS 不报错
user.id = 999;        // ✅ 能修改!

二、JavaScript 类成员变量修饰符(运行时,ES2022+)

ECMAScript 2022(ES13) 开始,JavaScript 原生支持 真正的私有字段(Private Fields) ,使用 # 前缀。

语法 含义 运行时是否私有 是否可被外部访问
#fieldName 私有字段 ✅ 是 ❌ 完全无法从类外访问
普通字段(无前缀) 公共字段 ❌ 否 ✅ 可自由访问

✅ 示例(JavaScript / TypeScript 均支持):

class User {
  #id;                    // 私有字段(JS 原生私有)
  name;                   // 公共字段

  constructor(name, id) {
    this.name = name;
    this.#id = id;        // 只能在类内部访问
  }

  getId() {
    return this.#id;      // ✅ OK
  }
}

const user = new User("Bob", 2);
console.log(user.name);   // ✅ "Bob"
console.log(user.#id);    // ❌ SyntaxError! 无法访问

关键优势
#id真正的私有,即使在运行时也无法绕过(除非用 Proxy 等 hack,但正常代码做不到)。


三、TypeScript 对 JS 私有字段的支持

TypeScript 完全支持 # 私有字段,并提供类型检查:

class User {
  #id: number;
  name: string;

  constructor(name: string, id: number) {
    this.name = name;
    this.#id = id;
  }

  getId(): number {
    return this.#id; // ✅ TS 知道这是 number
  }
}

🔸 此时你不需要private,因为 #id 已经是运行时私有。


四、对比总结

特性 TypeScript private JavaScript #field
作用时机 编译时(类型检查) 运行时(真实私有)
能否被外部访问 ✅ 能(JS 无保护) ❌ 不能
是否生成额外代码 ❌ 否 ✅ 是(保留 # 语法)
兼容性 所有 JS 环境(因被擦除) 需要 ES2022+ 或 Babel 转译
推荐场景 快速开发、内部项目 需要真正封装、库开发

五、最佳实践建议

✅ 优先使用 JavaScript 原生私有字段 #

  • 如果目标环境支持(现代浏览器 / Node.js 12+),优先用 #fieldName
  • 它提供真正的封装,避免“假装私有”的陷阱。

✅ 在 TypeScript 中:

  • 若需兼容旧环境 → 用 private(但要清楚它只是“纸面私有”)。
  • 若用现代环境 → 直接用 # ,无需 private

✅ 不要混用:

// ❌ 不推荐:语义重复且混乱
private #id; // 错误!不能同时用

readonly 仍是 TS 特有(JS 无等价物)

  • 可配合 # 使用:

    class Config {
      readonly #apiUrl: string;
      constructor(url: string) {
        this.#apiUrl = url; // 初始化后不可变(TS 检查)
      }
    }
    

六、补充:其他相关修饰符

修饰符 语言 说明
static TS & JS 静态成员(属于类,不属于实例)
abstract TS only 抽象类/方法(不能实例化)
declare TS only 声明属性存在(用于 .d.ts 或装饰器)

✅ 总结

需求 推荐方案
真正的私有字段 使用 JavaScript #fieldName(ES2022+)
仅开发时提醒(兼容旧环境) 使用 TypeScript private
只读属性 TypeScript readonly(JS 无原生支持)
公共字段 直接声明(TS/JS 均默认 public)

🎯 现代项目建议
# 实现私有,用 readonly 实现只读,放弃 private(除非必须兼容旧 JS)

这样既能获得类型安全,又能保证运行时封装性。

❌