发布于 2026-01-06 2 阅读
0

深入探索 TypeScript 基础知识:你可能不知道的 21 个 TypeScript 特性

深入探索 TypeScript 基础知识:你可能不知道的 21 个 TypeScript 特性

介绍

Lingo.dev,我编写了大量的 TypeScript 代码。我当然不是什么高手,但我确实会尝试使用一些超越基本类型的功能。

这篇文章介绍了一些功能(以及你可能想要使用它们的情况),以帮助你扩展知识,超越绝对的基础知识。

1. 只读数组、元组和as const断言

默认情况下,数组和对象都是可变的,TypeScript 会将字面值扩展为其通用类型。这使得 TypeScript 更难帮助你发现 bug 并提供准确的自动补全。

const colors = ["red", "green", "blue"];
// Type: string[] - could be any strings
colors.push("yellow"); // Allowed, might not be what you want

type Color = (typeof colors)[number]; // string (too general!)
Enter fullscreen mode Exit fullscreen mode

解决方案

用于as const将所有内容设为只读并保留字面类型,或readonly用于特定数组。

const colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]
colors.push("yellow"); // ✗ Error: can't modify readonly array

type Color = (typeof colors)[number]; // "red" | "green" | "blue" ✓

// Or for function parameters:
function display(items: readonly string[]) {
  items.push("x"); // ✗ Error: can't modify
  items.forEach(console.log); // ✓ OK: reading is fine
}
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 配置或不应更改的常量数据
  • 防止意外突变
  • 保留字面类型以获得更好的类型推断
  • 不应修改的函数参数

了解更多: TypeScript 文档:ReadonlyArray

2.keyof typeof对于对象作为常量枚举

TypeScript 枚举有一些怪癖,而且会生成 JavaScript 代码。有时候,你只想在对象中定义常量,并从中派生类型。

解决方案

组合as const(锁定字面值)、typeof(获取对象的类型)和keyof(获取键或值的并集)。

// Define your constants as a plain object
const STATUS = {
  PENDING: "pending",
  APPROVED: "approved",
  REJECTED: "rejected",
} as const; // Lock in the literal values

// Get a union of the values
type Status = (typeof STATUS)[keyof typeof STATUS];
// "pending" | "approved" | "rejected"

function setStatus(status: Status) {
  // TypeScript validates and autocompletes!
}

setStatus(STATUS.APPROVED); // ✓
setStatus("pending"); // ✓
setStatus("invalid"); // ✗ Error
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 替代枚举的方案,提供更好的 JavaScript 输出
  • 创建基于常量且派生类型的配置
  • 当您需要运行时值和编译时类型时

了解更多: TypeScript 文档:typeof 类型

3. 带标签的元组元素

元组类似[number, number, boolean]工作,但每个位置的含义并不明确。是“是”[width, height, visible]还是“否[x, y, enabled]”?

解决方案

给元组位置赋予有意义的名称,使其显示在编辑器的自动完成和错误消息中。

// Before: unclear what each number means
type Range = [number, number, boolean?];

// After: self-documenting
type Range = [start: number, end: number, inclusive?: boolean];

function createRange([start, end, inclusive = false]: Range) {
  // Your editor will show you the parameter names!
  return { start, end, inclusive };
}

createRange([1, 10, true]); // Clear what each argument means
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 基于元组的函数参数
  • 返回包含多个相关数据项的值
  • 任何位置含义不明确的元组

了解更多: TypeScript 文档:元组类型

4. 索引访问和元素类型提取

你有一个复杂的类型,只想引用其中一个属性的类型,或者提取数组中的内容,而不想重复自己。

解决方案

使用方括号表示法(Type["property"])访问属性类型,并[number]获取数组元素类型。

type User = {
  id: number;
  profile: {
    name: string;
    emails: string[];
  };
};

// Access nested property types
type ProfileType = User["profile"]; // { name: string; emails: string[] }
type NameType = User["profile"]["name"]; // string

// Extract array element type
type Email = User["profile"]["emails"][number]; // string
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 从现有类型派生类型(DRY 原则)
  • 提取数组/元组元素类型
  • 无需重新定义类型即可处理嵌套结构

了解更多: TypeScript 文档:索引访问类型

5. 用户自定义类型守卫(arg is T

你编写了一个函数来检查某个对象是否属于某个特定类型,但 TypeScript 无法理解该检查实际上缩小了类型范围。

function isPerson(x: unknown) {
  return typeof x === "object" && x !== null && "name" in x;
}

function greet(x: unknown) {
  if (isPerson(x)) {
    x.name; // ✗ Error: TypeScript still thinks x is 'unknown'
  }
}
Enter fullscreen mode Exit fullscreen mode

解决方案

使用类型谓词(arg is Type)告诉 TypeScript 你的函数执行类型检查。

type Person = { name: string; age: number };

function isPerson(x: unknown): x is Person {
  return (
    typeof x === "object" &&
    x !== null &&
    "name" in x &&
    typeof (x as any).name === "string"
  );
}

function greet(x: unknown) {
  if (isPerson(x)) {
    console.log(x.name); // ✓ TypeScript knows x is Person here!
  }
}
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 验证来自 API 或用户输入的数据
  • 类型安全的验证函数
  • 工会中的类型区分

了解更多: TypeScript 文档:类型谓词

6. 详尽检查never

你有一个联合类型(例如不同的形状或状态)和一个 switch 语句。之后,有人向该联合类型添加了一个新的变体,但忘记在 switch 语句中处理它。没有抛出任何错误——它只是默默地不起作用。

解决方案

添加一个default将值赋给某个never​​类型的 case。如果所有 case 都已处理,则默认值将无法访问。如果缺少某个 case,TypeScript 将报错,因为该值无法赋给某个类型never

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      // If all cases are handled, this is unreachable
      const _exhaustive: never = shape;
      throw new Error(`Unhandled shape: ${_exhaustive}`);
  }
}

// Later, someone adds triangle:
// type Shape = ... | { kind: "triangle"; base: number; height: number };
// ✓ TypeScript error in default case: triangle is not assignable to never!
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 关于歧视性工会的声明转变
  • 确保所有联合变体都得到处理。
  • 捕捉随着类型演变而出现的漏洞

了解更多: TypeScript 文档:穷尽性检查

7. 仅类型导入和导出(import type/ export type

有时你会从其他模块导入类型,但这些导入语句会出现在编译后的 JavaScript 代码中,即使它们仅用于类型检查。这可能会导致循环依赖或软件包膨胀。

解决方案

用于import type告诉 TypeScript:“这只是类型检查所必需的,请将其从 JavaScript 中完全删除。”

// Regular import - might end up in compiled JS
import { User } from "./types";

// Type-only import - guaranteed to be removed from JS
import type { User } from "./types";

// Mixed imports
import { saveUser, type User } from "./api";
//       ^^^^^^^^^  ^^^^^^^^^^^
//       value      type-only
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 防止循环依赖问题
  • 保持 JavaScript 包更小
  • 当使用需要显式类型导入的构建工具时(isolatedModules
  • 明确意图(这仅适用于类型,不适用于运行时代码)

了解更多: TypeScript 文档:导入类型

8. 非代码资源的外部模块声明

您导入了非 TypeScript 文件(例如图像、CSS 或数据文件),但 TypeScript 不知道它们应该是什么类型。

import logo from "./logo.svg"; // ✗ Error: Cannot find module
Enter fullscreen mode Exit fullscreen mode

解决方案

创建环境模块声明,告诉 TypeScript 如何对这些导入进行类型识别。

// In a .d.ts file (like global.d.ts or declarations.d.ts)
declare module "*.svg" {
  const url: string;
  export default url;
}

declare module "*.css" {
  const classes: { [key: string]: string };
  export default classes;
}

// Now these work:
import logo from "./logo.svg"; // logo: string
import styles from "./app.css"; // styles: { [key: string]: string }
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 输入图像、字体、样式
  • 构建工具无法处理 JSON 或数据文件
  • 任何非 TypeScript 资源都会被打包程序处理

了解更多: TypeScript 文档:模块声明模板

9.satisfies操作员

有时你希望 TypeScript 检查对象是否与类型匹配,但你希望 TypeScript 记住你使用的具体值(而不仅仅是它们是字符串或数字)。

// Without satisfies - loses specific information
const routes: Record<string, string> = {
  home: "/",
  profile: "/users/:id",
};
// routes.profile is just 'string', not the specific "/users/:id"
Enter fullscreen mode Exit fullscreen mode

解决方案

satisfies检查你的对象是否符合类型,而不会改变 TypeScript 记住的类型信息。

const routes = {
  home: "/",
  profile: "/users/:id",
} satisfies Record<string, `/${string}`>; // Must be strings starting with "/"

// routes.profile is still the literal "/users/:id" - exact value preserved!
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 配置对象,其中既需要验证也需要特定值类型
  • 当您需要精确值的自动完成功能,而不仅仅是通用类型时

了解更多: TypeScript 文档:satisfies 运算符

10. 断言函数(assertsasserts x is T

有时你需要一个函数,当条件不满足时抛出错误。类型保护(如上所示)仅在if语句中有效——它们不会影响函数调用之后的代码。

function assertNotNull(x: unknown) {
  if (x == null) throw new Error("Value is null!");
}

const data: string | null = getValue();
assertNotNull(data);
// TypeScript still thinks data might be null here
Enter fullscreen mode Exit fullscreen mode

解决方案

断言函数用于asserts告诉 TypeScript:“如果此函数返回(不抛出异常),则条件为真。”

function assertNotNull<T>(x: T): asserts x is NonNullable<T> {
  if (x == null) throw new Error("Value is null!");
}

const data: string | null = getValue();
assertNotNull(data);
// ✓ TypeScript now knows data is definitely string here!
data.toUpperCase(); // Safe to use
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 验证函数在失败时抛出异常
  • 强制执行运行时不变性
  • 在函数边界处进行早期错误检查

了解更多: TypeScript 文档:断言函数

11. 字符串模式的模板字面量类型

假设你有一些事件名称,例如,,,"user:login"等等。你希望 TypeScript 能够自动补全这些名称并检测拼写错误,但是数量太多,无法手动列出。"user:logout""post:create"

解决方案

模板字面量类型允许您使用与 JavaScript 模板字符串相同的语法来描述字符串模式。

// Generate all combinations automatically
type EventName = `${"user" | "post"}:${"create" | "delete"}`;
// Result: "user:create" | "user:delete" | "post:create" | "post:delete"

function trackEvent(event: EventName) {
  // TypeScript will autocomplete and validate the event names!
}

trackEvent("user:create"); // ✓ OK
trackEvent("user:update"); // ✗ Error - not a valid combination
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 遵循某种模式的 API 路由或事件名称
  • 带有前缀/后缀的 CSS 类名
  • 任何结构化字符串格式(例如数据库表名、文件路径)

了解更多: TypeScript 文档:模板字面量类型

12. 分布式条件类型

string | number | null你想通过对每个成员应用逻辑来过滤或转换联合类型(例如)。

解决方案

当被检查的类型是“裸露的”(没有被包装在另一个类型中)时,条件类型会自动在联合体上进行分布。

// Remove null and undefined from a union
type NonNullish<T> = T extends null | undefined ? never : T;

// This distributes: checks each member separately
type Clean = NonNullish<string | number | null>;
// string | number (null was filtered out)

// Extract only function types
type FunctionsOnly<T> = T extends (...args: any[]) => any ? T : never;
type Fns = FunctionsOnly<string | ((x: number) => void) | boolean>;
// (x: number) => void
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 过滤联合类型
  • 建筑公用设施类型,例如ExcludeExtract
  • 对工会的每个成员进行不同程度的改造

了解更多: TypeScript 文档:分布式条件类型

13.infer在条件语句中捕获类型

你需要提取复杂类型的一部分(例如“这个函数返回什么类型?”或“这个数组里有什么?”)。

解决方案

用于infer创建类型变量,该变量捕获您正在检查的类型的一部分。

// Extract the return type of a function
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;

type MyFunc = (x: number) => string;
type Result = ReturnType<MyFunc>; // string

// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;

type Numbers = ElementType<number[]>; // number
type Mixed = ElementType<(string | boolean)[]>; // string | boolean
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 从函数中提取参数或返回类型
  • 从数组或元组中获取元素类型
  • 解析模板字面量或复杂结构中的类型

了解更多: TypeScript 文档:条件类型中的类型推断

14. 映射类型修饰符(+readonly,,-?等)

有时你需要将现有类型中的所有属性都设为必需(移除 `required` 属性?),或者将所有属性都设为可变(移除 `mutable` 属性readonly)。手动重写每个属性非常繁琐。

解决方案

映射类型可以添加(+)或删除(-readonly和可选的(?)修饰符。

// Remove readonly from all properties
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove optional (?) from all properties
type Required<T> = {
  [K in keyof T]-?: T[K];
};

type Config = Readonly<{ port?: number; host?: string }>;
type EditableConfig = Mutable<Required<Config>>; // { port: number; host: string }
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 创建只读类型的可编辑版本
  • 使所有属性都符合验证函数的要求
  • 构建用于转换属性修饰符的实用程序类型

了解更多: TypeScript 文档:映射修饰符

15. 映射类型中的键重映射(as

你想通过更改属性名称(例如删除前缀或过滤掉某些属性)来转换对象类型,但映射类型通常会保留相同的键。

解决方案

在映射类型中使用as,可以在遍历键时转换键。

// Remove private properties (those starting with _)
type RemovePrivate<T> = {
  [K in keyof T as K extends `_${string}` ? never : K]: T[K];
};

type WithPrivate = { name: string; _secret: number };
type Public = RemovePrivate<WithPrivate>; // { name: string }

// Add a prefix to all keys
type Prefixed<T> = {
  [K in keyof T as `app_${string & K}`]: T[K];
};

type Original = { id: number; name: string };
type Result = Prefixed<Original>; // { app_id: number; app_name: string }
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 通过过滤掉内部属性来创建类型的公共版本
  • 命名规则转换(驼峰式命名法到蛇形命名法)
  • 基于关键模式筛选对象类型

了解更多: TypeScript 文档:映射类型中的键重映射

16. 常量类型参数

当你将数组传递给泛型函数时,TypeScript 通常会将其“扩展”为通用数组类型,从而丢失有关具体值的信息。

function identity<T>(value: T) {
  return value;
}

const pair = identity([1, 2]); // Type is number[], not [1, 2]
Enter fullscreen mode Exit fullscreen mode

解决方案

在类型参数前添加const,告诉 TypeScript:“尽可能保持具体”。

function identity<const T>(value: T) {
  return value;
}

const pair = identity([1, 2]); // Type is [1, 2] - exact tuple preserved!
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 能够精确保留数组/元组结构的函数
  • 构建器函数,用于跟踪通过转换的字面值

了解更多: TypeScript 文档:const 类型参数

17. 可变参数元组类型和扩展

如何编写一个需要接受不同数量参数并能跟踪每个参数类型的函数?

解决方案

可变参数元组允许你处理可以增长或缩小的类型列表。可以把它想象...成“把这个类型列表展开到这里”。

// Type that adds an element to the end of a tuple
type Push<T extends unknown[], U> = [...T, U];

type Result = Push<[string, number], boolean>; // [string, number, boolean]

// Real example: Typing a function wrapper
function logged<Args extends unknown[], Return>(
  fn: (...args: Args) => Return,
): (...args: Args) => Return {
  return (...args) => {
    console.log("Calling with:", args);
    return fn(...args);
  };
}
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 在保留函数参数类型的同时封装函数
  • 创建类型安全的函数组合实用程序
  • 构建元组操作类型

了解更多: TypeScript 文档:元组类型

18.this函数中的参数

当函数使用 ` <type>` 时this,TypeScript 无法确定 `<type>`this应该是什么类型。这会导致方法、回调函数和事件处理程序出现问题。

function setName(name: string) {
  this.name = name; // ✗ Error: 'this' has type 'any'
}
Enter fullscreen mode Exit fullscreen mode

解决方案

添加一个显式this参数(运行时不计入实际参数)来指定this应该是什么。

interface Model {
  name: string;
  setName(this: Model, newName: string): void;
}

const model: Model = {
  name: "Initial",
  setName(this: Model, newName: string) {
    this.name = newName; // ✓ TypeScript knows what 'this' is!
  },
};

model.setName("Updated"); // Works
const fn = model.setName;
fn("Test"); // ✗ Error: 'this' context is wrong
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 依赖于以下方法this
  • 事件处理程序回调
  • .call()设计用于通过以下方式调用的函数:.apply()

了解更多: TypeScript 文档:this 参数

19.unique symbol用于名义型类型

TypeScript 使用“结构类型”——结构相同的两个类型被视为相同。有时你需要结构相同但逻辑不同的类型(例如 UserID 和 ProductID,它们都是字符串)。

type UserId = string;
type ProductId = string;

function getUser(id: UserId) {
  /* ... */
}

const productId: ProductId = "prod-123";
getUser(productId); // ✗ We want this to be an error, but it's not!
Enter fullscreen mode Exit fullscreen mode

解决方案

用于unique symbol创建“品牌”,即使结构相同,也能使类型不兼容。

declare const USER_ID: unique symbol;
type UserId = string & { [USER_ID]: true };

declare const PRODUCT_ID: unique symbol;
type ProductId = string & { [PRODUCT_ID]: true };

function getUser(id: UserId) {
  /* ... */
}

const productId = "prod-123" as ProductId;
getUser(productId); // ✓ Now this IS an error!
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 防止混淆不同类型的 ID(用户 ID、订单 ID 等)
  • 创建不会意外替换的“名义”类型
  • 用于注册表或依赖注入的类型安全键

了解更多: TypeScript 文档:唯一符号

20. 模块扩充和声明合并

您正在使用第三方库,需要向其类型添加属性(例如添加自定义配置选项),但您无法编辑库的代码。

解决方案

使用模块扩展功能,从外部向现有接口或模块添加内容。

// In your own .d.ts file
declare module "express" {
  // Add to Express's Request interface
  interface Request {
    user?: { id: string; name: string };
  }
}

// Now TypeScript knows about req.user in your Express handlers!
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 使用自定义属性扩展库类型
  • 为未完全类型化的库功能添加类型
  • 插件系统,您可以在其中注册新功能

了解更多: TypeScript 文档:模块扩展

21. 构造函数签名和抽象“可更新”类型

你想编写一个函数,它接受一个(而不是实例),并创建该类的实例。如何输入“可以用以下方式构造的东西new”?

function createInstance(SomeClass: ???) {
  return new SomeClass();
}
Enter fullscreen mode Exit fullscreen mode

解决方案

使用构造函数签名:new (...args: any[]) => T描述可以构造的事物。

// Type describing a constructor
type Constructor<T = unknown, Args extends unknown[] = any[]> = new (
  ...args: Args
) => T;

function createInstance<T>(Ctor: Constructor<T>): T {
  return new Ctor();
}

class User {
  name = "Unknown";
}

const user = createInstance(User); // user: User ✓

// More complex: factory with specific constructor arguments
function createPair<T>(
  Ctor: Constructor<T, [string, number]>,
  name: string,
  age: number,
): T {
  return new Ctor(name, age);
}
Enter fullscreen mode Exit fullscreen mode

何时使用

  • 依赖注入框架
  • 创建实例的工厂函数
  • 能够将类作为值的通用代码
  • 测试模拟构造函数的实用程序

了解更多: TypeScript 文档:接口中的构造函数签名

文章来源:https://dev.to/lingodotdev/beyond-the-basics-21-typescript-features-you-might-not-know-about-1dbn