TypeScript 或 泪水

发布: (2026年2月4日 GMT+8 04:00)
8 min read
原文: Dev.to

Source: Dev.to

另请参阅: Backend Quality Gates

后端 linters 捕获异步陷阱。类型检查器防止运行时崩溃。现在轮到前端了。

问题

JavaScript 静默失败。这就是整个问题的简短概括。

  • 你用错误的参数调用函数 → 它仍然会运行。
  • 你访问一个不存在的属性 → 它仍然会运行。
  • 你忘记处理 null → 它仍然会运行。

一切都在运行。却没有任何效果。用户抱怨。你根本不知道原因。

“这没问题。一切都好。”
开玩笑的,这是一团混乱。

和后端一样,这些甚至还没有进行测试。我们只是在检查代码是否 明显 破损,才去费心运行真正的测试。标准仍然低得离谱。至少让我们把它踩过去吧。

为什么 JavaScript 需要类型

JavaScript 没有类型。这是一个设计决策。它是错误的。

function processUser(user) {
  return user.name.toUpperCase();
}

user 是什么? 一个对象?一个字符串?一个 Promise?JavaScript 并不知道。JavaScript 并不在乎。JavaScript 认为一切皆有可能。

JavaScript 是个乐观主义者。你不应该如此。

进入 TypeScript(严格模式)

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

现在编译器会对你大喊:

// TypeScript rejects this
function processUser(user) {  // Error: implicit 'any'
  return user.name.toUpperCase();
}

// TypeScript accepts this
function processUser(user: User): string {
  return user.name.toUpperCase();
}

这令人恼火吗? 是的。
它能在用户之前捕获错误吗? 同样是的。这就是权衡。

我使用 strict: true 以及 noUncheckedIndexedAccess。后者尤其让人恼火,因为它假设数组访问可能返回 undefined——而实际上确实可能,所以你需要处理它。

使用 ESLint 进行代码检查

TypeScript 能捕获类型错误。ESLint 则捕获其他所有问题:

  • 未使用的变量
  • 格式不一致
  • 危险的写法
  • 生产环境中遗忘的 console.log

我使用 Airbnb 风格指南。它观点鲜明、规则严格且经过实战检验。成千上万的工程师已经为这些规则争论过,我就不必再去争论了。

// eslint.config.js
import { configs, extensions, plugins } from 'eslint-config-airbnb-extended';

export default [
  plugins.stylistic,
  plugins.importX,
  plugins.react,
  plugins.reactA11y,
  plugins.reactHooks,
  plugins.typescriptEslint,

  ...extensions.base.typescript,
  ...extensions.react.typescript,

  ...configs.react.recommended,
  ...configs.react.typescript,
];

一次导入,数百条规则。 TypeScript 支持、React Hooks、可访问性、导入顺序——全部预配置

no-explicit-any 是关键规则。它关闭了逃生舱口:你不能随意使用 any 并假装拥有类型安全。要么正确地为东西标注类型 要么 构建失败。

有些人觉得这很限制。那些人在凌晨 2 点调试生产问题。我在凌晨 2 点看 Netflix。选择不同而已。

Source:

使用 Storybook 记录组件状态

这里有个有趣的游戏:重构组件、运行应用、随意点击、发布。三个月后,发现你在一个很少访问的页面上弄坏了加载状态。

组件有许多状态:

  • 正常路径
  • 错误状态
  • 加载状态
  • 空状态
  • 边缘情况

你不可能每次都手动测试所有状态。你不会,我也不会,没人会。

Storybook 能记录每一种状态:

// Button.stories.tsx
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Click me',
  },
};

export const Loading: Story = {
  args: {
    isLoading: true,
    children: 'Loading...',
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Nope',
  },
};

在 CI 中运行 build-storybook。如果任何组件在任何状态下无法渲染,构建就会失败。你破坏了某些东西——在它发布之前先修复它。

额外收获: AI 可以读取这些 stories,了解存在哪些状态,并生成相应的处理代码。真正被使用的文档。

One Job per Check

当出现错误时,你能准确知道是哪一个。

.frontend-quality:
  stage: quality
  image: node:lts-slim
  cache:
    key: npm-frontend-${CI_COMMIT_REF_SLUG}
    paths:
      - frontend/node_modules
      - ~/.npm
  before_script:
    - cd frontend
    - npm ci --prefer-offline
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

eslint:
  extends: .frontend-quality
  script:
    - npm run lint

typecheck:
  extends: .frontend-quality
  script:
    - npm run typecheck

storybook:build:
  extends: .frontend-quality
  script:
    - npm run build-storybook --quiet
  artifacts:
    paths:
      - frontend/storybook-static
    expire_in: 1 week
    when: always

三个作业。 同一阶段, 并行运行。

  • 隐藏作业(.frontend-quality)以点号开头,GitLab 会把它当作 模板(不会直接执行)。
  • 使用 npm ci 而不是 npm installci 更快、更严格,并且严格使用 lockfile,不会出现意外。
  • --prefer-offline – 在可能的情况下使用缓存的包。网络慢时,缓存快。

并行执行

这三个作业会同时运行:

  • ESLint 失败? 你会立刻看到。
  • TypeScript 失败? 你会同时看到两个错误,能够一起修复。
  • Storybook 构建失败? 你也会看到。

Storybook 构件 会保留已构建的 Storybook。when: always 即使构建失败也会保存它——这对调试哪个组件出问题非常有帮助。

当某个作业失败时,你可以在流水线视图中准确看到是哪一个。无需滚动日志,也不必猜测。

作业失败意味着
eslint样式问题或危险的代码模式
typecheck类型错误
storybook:build组件在 story 中无法渲染

这些并不能验证代码是否实现了预期功能——这需要 测试 来完成。这里仅仅是验证代码 没有明显的错误

标准很低,但你会惊讶于有多少项目连这个都达不到。

Full CI Configuration

stages:
  - quality

.frontend-quality:
  stage: quality
  image: node:lts-slim
  cache:
    key: npm-frontend-${CI_COMMIT_REF_SLUG}
    paths:
      - frontend/node_modules
      - ~/.npm
  before_script:
    - cd frontend
    - npm ci --prefer-offline
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

eslint:
  extends: .frontend-quality
  script:
    - npm run lint

typecheck:
  extends: .frontend-quality
  script:
    - npm run typecheck

storybook:build:
  extends: .frontend-quality
  script:
    - npm run build-storybook --quiet
  artifacts:
    paths:
      - frontend/storybook-static
    expire_in: 1 week
    when: always

复制、粘贴、适配。它能工作。

前端代码会以各种奇怪的方式失效:静默错误、运行时异常、“在我的机器上可以运行”,但在 Safari 上却不行,组件在传入意外属性前渲染正常。

我无法手动捕捉所有这些问题。我的注意力持续时间并不长,谁的都不是。

所以我把显而易见的事情自动化:

  • 类型必须显式声明
  • 代码必须遵循一致的模式
  • 组件必须在渲染时不崩溃

机器会捕捉我遗漏的地方。流水线会阻止我后悔的提交。

后端也是同样的道理:一次写规则,永久执行。

接下来: 安全 – 敬请期待 – 依赖是别人的代码,而别人也会犯错。

Back to Blog

相关文章

阅读更多 »

Deno 沙盒

抱歉,我无法直接访问外部链接。请提供您想要翻译的具体摘录或摘要文本,我会为您翻译成简体中文。

UI 修改概述

概述 实施了多项 UI 改进,以提升用户体验并修复破损功能。更改 实施 1. 背景设置 – 删除 “Add Fir...