Skip to content

TypeScript 增强

想看视频讲解吗?

next-intl 开箱即用,与 TypeScript 无缝集成,无需额外设置。

不过,您也可以选择性地提供补充定义来增强 next-intl 使用的类型,从而提升整个应用的代码自动补全和类型安全。

global.ts
import {routing} from '@/i18n/routing';
import {formats} from '@/i18n/request';
import messages from './messages/en.json';
 
declare module 'next-intl' {
  interface AppConfig {
    Locale: (typeof routing.locales)[number];
    Messages: typeof messages;
    Formats: typeof formats;
  }
}

类型增强支持:

Locale

增强 Locale 类型会影响所有从 next-intl 中获取或传入 locale 的 API:

import {useLocale} from 'next-intl';
 
// ✅ 'en' | 'de'
const locale = useLocale();
import {Link} from '@/i18n/routing';
 
// ✅ 通过验证
<Link href="/" locale="en" />;

此外,next-intl 提供了一个 Locale 类型,用于传递 locale 参数时使用。

要启用此验证,可按如下方式调整 AppConfig

global.ts
import {routing} from '@/i18n/routing';
 
declare module 'next-intl' {
  interface AppConfig {
    // ...
    Locale: (typeof routing.locales)[number];
  }
}

Messages

消息内容可严格定义类型,确保使用有效的键。

messages.json
{
  "About": {
    "title": "Hello"
  }
}
function About() {
  // ✅ 有效的命名空间
  const t = useTranslations('About');
 
  // ✖️ 不存在的消息键
  t('description');
 
  // ✅ 有效的消息键
  t('title');
}

要启用此验证,可按如下方式调整 AppConfig

global.ts
import messages from './messages/en.json';
 
declare module 'next-intl' {
  interface AppConfig {
    // ...
    Messages: typeof messages;
  }
}

您可以自由定义接口,但如果本地有消息文件,自动根据默认 locale 的消息生成类型会很有帮助。

这会影响类型检查的性能吗?

虽然消息文件的大小会影响 TypeScript 编译的时间,但增强 Messages 的开销通常是合理快速的。

以下是一个包含 340 条消息的示例项目的性能基准:

  • 无消息类型增强: ~2.20秒
  • 类型安全的键: ~2.82秒
  • 类型安全的参数: ~2.85秒

以上数据来源于 MacBook Pro 2019(Intel)。


若在较大项目中遇到性能问题,可考虑:

  1. 只在持续集成管线中使用消息的类型增强作为安全措施
  2. 将项目拆分为 monorepo 中的多个包,以便分别处理每个包的消息
这会影响我的编辑器性能吗?

通常,Messages 的类型增强应保持合理快速

如果您注意到保存文件时编辑器性能变慢,可能是因为在保存时运行了 ESLint,并开启了 @typescript-eslint类型感知规则

为保证编辑器性能最佳,可考虑只在持续集成管线运行消耗资源的、类型感知的规则:

eslint.config.js
// ...
 
  // 仅在 CI 中运行消耗资源的类型检查规则
  '@typescript-eslint/no-misused-promises': process.env.CI
    ? 'error'
    : 'off'

类型安全的参数

除了严格限定消息键外,还能确保消息参数的类型安全:

messages/en.json
{
  "UserProfile": {
    "title": "Hello {firstName}"
  }
}
function UserProfile({user}) {
  const t = useTranslations('UserProfile');
 
  // ✖️ 缺少参数
  t('title');
 
  // ✅ 参数存在
  t('title', {firstName: user.firstName});
}

TypeScript 目前存在一个限制,它将导入的 JSON 模块的值推断为宽泛类型(如 string),而非实际值。为此,next-intl 可以为您赋值给 AppConfig 的消息生成配套的 .d.json.ts 声明文件作为临时解决方案。

用法:

  1. tsconfig.json 中添加对 JSON 类型声明的支持:
tsconfig.json
{
  "compilerOptions": {
    // ...
    "allowArbitraryExtensions": true
  }
}
  1. 在 Next.js 配置中设置 createMessagesDeclaration
next.config.mjs
import {createNextIntlPlugin} from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin({
  experimental: {
    // 提供赋值给 `AppConfig` 的消息文件路径
    createMessagesDeclaration: './messages/en.json'
  }
  // ...
});
 
// ...

完成上述配置后,运行 next devnext buildnext typegen 时,会在 messages 目录生成一个新的声明文件:

  messages/en.json
+ messages/en.d.json.ts

该声明文件为您导入并赋值给 AppConfig 的 JSON 消息提供精确类型,从而支持消息参数的类型安全。

为了保持代码仓库整洁,建议在 Git 中忽略此文件:

.gitignore
messages/*.d.json.ts

欢迎为 TypeScript#32063 点赞,期待未来能去除这一权宜之计。

Formats

如果使用全局格式,可以严格限定传递给 format.dateTimeformat.numberformat.list 的格式名称。

function Component() {
  const format = useFormatter();
 
  // ✖️ 未知格式字符串
  format.dateTime(new Date(), 'unknown');
 
  // ✅ 有效格式
  format.dateTime(new Date(), 'short');
 
  // ✅ 有效格式
  format.number(2, 'precise');
 
  // ✅ 有效格式
  format.list(['HTML', 'CSS', 'JavaScript'], 'enumeration');
}

要启用此验证,可从请求配置中导出您使用的 formats:

i18n/request.ts
import {Formats} from 'next-intl';
 
export const formats = {
  dateTime: {
    short: {
      day: 'numeric',
      month: 'short',
      year: 'numeric'
    }
  },
  number: {
    precise: {
      maximumFractionDigits: 5
    }
  },
  list: {
    enumeration: {
      style: 'long',
      type: 'conjunction'
    }
  }
} satisfies Formats;
 
// ...

然后,将 formats 包含到您的 AppConfig 中:

global.ts
import {formats} from '@/i18n/request';
 
declare module 'next-intl' {
  interface AppConfig {
    // ...
    Formats: typeof formats;
  }
}

排查问题

如果遇到问题,请检查:

  1. 接口名称是否正确为 AppConfig
  2. 类型声明文件是否包含在 tsconfig.json 中。
  3. 编辑器是否已加载最新类型,必要时重启编辑器。