通过 TypeScript 类型体操学习日语语法

尽管一个用于人类交流,一个服务于机器指令,自然语言和编程语言却共享着相似的基本原理。两者均由语法规则、结构限制和组合方式所定义。

举例来说,日语以其严(luo)谨(suo)的语法规则和丰富的变形系统而闻名,而 TypeScript 的类型系统则因其表达能力和灵活性而受到推崇。当我们将这两个领域结合起来,会发生什么呢?本文将探索如何利用 TypeScript 的高级泛型编程来建模日语语法结构,让编程语言成为辅助语言学习的新工具。

由于全文较长,因此这里先预告几个后文中将演示的日语例句:

  • 疑问句「为什么要演奏春日影?」
  • 条件句「辛美尔的话也会这么做的」
  • 组合句「好时代,来临啦」

下面,让我们从基础知识开始循序渐进吧。

从命令式编程到类型编程

在深入日语语法之前,我们可以先通过一个简单的英语动词变形例子,来理解如何将常规的 JavaScript 函数转换为 TypeScript 类型级别的实现。

首先,下面是一个简单的 JavaScript 函数,它根据第二个参数决定给英语动词添加 "ed" 还是 "ing"

javascript 复制代码
function conjugateEnglishVerb(verb, form) {
  if (form === "past") {
    return verb + "ed";
  } else if (form === "progressive") {
    return verb + "ing";
  } else {
    return verb;
  }
}

// 使用示例
conjugateEnglishVerb("walk", "past"); // "walked"
conjugateEnglishVerb("talk", "progressive"); // "talking"

这个函数在运行时接收参数并返回结果。这是典型的命令式编程方法,即通过一系列明确的指令告诉计算机「如何」执行任务。现在,让我们将这个逻辑转换到 TypeScript 的类型系统中:

typescript 复制代码
// 定义可能的变形形式
type EnglishVerbForm = "base" | "past" | "progressive";

// 类型级别的动词变形函数
type ConjugateEnglishVerb<
  Verb extends string,
  Form extends EnglishVerbForm
> = Form extends "past"
  ? `${Verb}ed`
  : Form extends "progressive"
  ? `${Verb}ing`
  : Verb;

// 类型级别的使用示例
type WalkedVerb = ConjugateEnglishVerb<"walk", "past">; // 结果: "walked"
type TalkingVerb = ConjugateEnglishVerb<"talk", "progressive">; // 结果: "talking"

注意这里的关键转换:

  1. 函数参数变成了泛型参数 <Verb extends string, Form extends EnglishVerbForm>
  2. if/else 条件语句变成了条件类型 Form extends "past" ? ... : ...
  3. 字符串拼接 verb + "ed" 变成了模板字符串类型 ${Verb}ed
  4. 函数调用变成了类型声明和实例化

这个简单的例子用到了 TypeScript 类型系统中的三个核心工具:

  • 泛型:用于参数化类型
  • 条件类型:用于根据条件选择不同的类型
  • 模板字符串类型:用于在类型级别操作字符串

对比这两个例子可以感受到,声明式的类型编程相比命令式编程,在与计算机沟通的方式上有根本区别:

  • 命令式编程告诉计算机「如何做」,即通过一系列明确的步骤执行任务。
  • 而类型编程则是在描述「是什么」,即声明类型之间的关系和约束。我们不再发出指令序列,而是构建一个类型关系网络,让编译器在编译时自行推导结果。

这种范式转变使我们能够在程序运行前就验证复杂规则的正确性,将运行时错误转变为编译时错误,非常适合建模人类语法这种(纸面上)具有严格结构约束的系统。

扩展语法规则特例

当然,实际的语言规则要比上面的简单变换复杂得多,存在着许多人为规定的特例。让我们稍微扩展英语动词变形的例子,考虑一些特殊规则:

typescript 复制代码
// 更复杂的英语动词变形
type ConjugateEnglishVerbAdvanced<
  Verb extends string,
  Form extends EnglishVerbForm
> = Form extends "past"
  ? Verb extends `${infer Base}e`
    ? `${Base}ed` // 如果以 e 结尾,只加 d
    : Verb extends `${infer Base}y`
    ? `${Base}ied` // 如果以 y 结尾,变 y 为 ied
    : `${Verb}ed`
  : Form extends "progressive"
  ? Verb extends `${infer Base}e`
    ? `${Base}ing` // 如果以 e 结尾,去掉 e 再加 ing
    : `${Verb}ing`
  : Verb;

// 示例
type MovedVerb = ConjugateEnglishVerbAdvanced<"move", "past">; // "moved",不是 "moveed"
type CriedVerb = ConjugateEnglishVerbAdvanced<"cry", "past">; // "cried",不是 "cryed"
type MovingVerb = ConjugateEnglishVerbAdvanced<"move", "progressive">; // "moving",不是 "moveing"

这里我们使用了 infer 关键字来从类型表达式(即所谓的「模式」)中抽取出特定的片段,并在结果中重用它。这就是类型系统中名为模式匹配的强大工具。

日语动词变形基础:了解五段动词

对于没有接触过日语的读者,这里先简单介绍一下日语动词变形的基本概念。

在日语中,动词会根据时态、语气、礼貌程度等因素发生变形。这与英语中的 "walk/walked/walking" 类似,但日语的变形系统更加系统化且规则丰富。

日语的动词主要分为三类:五段动词、一段动词和不规则动词。其中,五段动词是最常见也是变形规则最复杂的一种。所谓的「五段」来源于传统日语语法中的元音行变化(あ、い、う、え、お五行),意味着这类动词在变形时词尾会在这五个行中变化。

以「買う」(买)这个五段动词为例,它在几种不同形式下的变化如下:

  • 基本形(字典形):買う (kau)
  • て形(连接形):買って (katte)
  • た形(过去式):買った (katta)
  • ない形(否定形):買わない (kawanai)
  • 命令形:買え (kae)

如果用 JavaScript 函数来实现这种变形逻辑,大致会是这样:

javascript 复制代码
function conjugateGodanVerb(stem, ending, form) {
  if (form === "て形") {
    if (ending === "う" || ending === "つ" || ending === "る") {
      return stem + "って";
    } else if (ending === "く") {
      return stem + "いて";
    } else if (ending === "ぐ") {
      return stem + "いで";
    } else if (ending === "す") {
      return stem + "して";
    } else if (ending === "ぬ" || ending === "ぶ" || ending === "む") {
      return stem + "んで";
    }
  } else if (form === "た形") {
    if (ending === "う" || ending === "つ" || ending === "る") {
      return stem + "った";
    } else if (ending === "く") {
      return stem + "いた";
    } else if (ending === "ぐ") {
      return stem + "いだ";
    } else if (ending === "す") {
      return stem + "した";
    } else if (ending === "ぬ" || ending === "ぶ" || ending === "む") {
      return stem + "んだ";
    }
  }
  // 其他形式的变形...
  return stem + ending; // 默认返回字典形
}

// 使用示例
console.log(conjugateGodanVerb("買", "う", "て形")); // "買って"
console.log(conjugateGodanVerb("書", "く", "た形")); // "書いた"

这个函数展示了五段动词变形的基本逻辑:根据词尾(ending)和目标形式(form)选择相应的变形规则。虽然它确实能处理五段动词的变形,但这种命令式的 if-else 实现有一些明显的缺点:

  • 首先,它只能在运行时检查并处理错误,无法在编译阶段提供语法正确性保证。
  • 其次,这种函数难以与更大的语言结构系统集成,缺乏组合性。
  • 最后,随着规则增加,嵌套的条件判断会导致代码复杂度急剧上升,维护困难。

相比之下,TypeScript 的类型系统提供了一种声明式的方法,能够在编译期捕获错误,并以更优雅、更具表达力的方式描述语言规则间的关系。通过将这些变形规则提升到类型级别,我们可以构建一个能静态验证日语语法正确性的系统,这正是下文中将要展示的方法。

使用类型编程实现日语动词变形

有了上面这些铺垫,我们现在可以开始构建日语动词变形的类型系统了。我们先从简单的例子开始:

typescript 复制代码
// 定义简化的日语动词类型
type SimpleJapaneseVerb = {
  type: "godan" | "ichidan";
  stem: string;
  ending: string;
};

// 定义变形形式
type JapaneseVerbForm = "辞書形" | "て形" | "た形";

// 简化的日语动词变形
type ConjugateSimpleJapaneseVerb<
  V extends SimpleJapaneseVerb,
  Form extends JapaneseVerbForm
> = Form extends "辞書形"
  ? `${V["stem"]}${V["ending"]}`
  : Form extends "て形"
  ? V["type"] extends "godan"
    ? V["ending"] extends "う"
      ? `${V["stem"]}って`
      : V["ending"] extends "く"
      ? `${V["stem"]}いて`
      : `${V["stem"]}${V["ending"]}`
    : V["type"] extends "ichidan"
    ? `${V["stem"]}て`
    : `${V["stem"]}${V["ending"]}`
  : Form extends "た形"
  ? V["type"] extends "godan"
    ? V["ending"] extends "う"
      ? `${V["stem"]}った`
      : V["ending"] extends "く"
      ? `${V["stem"]}いた`
      : `${V["stem"]}${V["ending"]}`
    : V["type"] extends "ichidan"
    ? `${V["stem"]}た`
    : `${V["stem"]}${V["ending"]}`
  : `${V["stem"]}${V["ending"]}`;

// 示例
type KauVerb = { type: "godan"; stem: "買"; ending: "う" };
type KauTeForm = ConjugateSimpleJapaneseVerb<KauVerb, "て形">; // "買って"
type KauTaForm = ConjugateSimpleJapaneseVerb<KauVerb, "た形">; // "買った"

type TaberuVerb = { type: "ichidan"; stem: "食べ"; ending: "る" };
type TaberuTeForm = ConjugateSimpleJapaneseVerb<TaberuVerb, "て形">; // "食べて"
type TaberuTaForm = ConjugateSimpleJapaneseVerb<TaberuVerb, "た形">; // "食べた"

现在,我们可以扩展这个模型,以更精确地反映日语动词的分类和变形规则。我们将使用接口继承来表示不同类型的动词及其特性:

typescript 复制代码
// 动词基础接口
interface Verb {
  dictionary: string;
}

// 五段动词
interface GodanVerb extends Verb {
  stem: string;
  ending: "う" | "く" | "ぐ" | "す" | "つ" | "ぬ" | "ぶ" | "む" | "る";
}

// 一段动词
interface IchidanVerb extends Verb {
  stem: string;
  ending: "る";
}

// 不规则动词
interface IrregularVerb extends Verb {
  dictionary: "する" | "来る";
}

// 定义变形形式
type ConjugationForm = "て形" | "た形" | "ない形" | "辞書形" | "命令形";

// 更完整的日语动词变形系统
type ConjugateVerb<
  T extends Verb,
  Form extends ConjugationForm
> = T extends GodanVerb
  ? GodanConjugation<T, Form>
  : T extends IchidanVerb
  ? IchidanConjugation<T, Form>
  : T extends IrregularVerb
  ? IrregularConjugation<T, Form>
  : never;

// 示例
type 買う = GodanVerb & { stem: "買"; ending: "う" };
type 買うて形 = ConjugateVerb<買う, "て形">; // 買って
type 買うた形 = ConjugateVerb<買う, "た形">; // 買った

type 食べる = IchidanVerb & { stem: "食べ"; ending: "る" };
type 食べるて形 = ConjugateVerb<食べる, "て形">; // 食べて
type 食べるた形 = ConjugateVerb<食べる, "た形">; // 食べた

这种方法展示了 TypeScript 类型系统的强大表达能力。通过使用泛型、条件类型和模板字符串类型,我们可以在编译时验证和转换日语动词,就像在运行时使用函数一样。

在接下来的部分中,我们将继续使用这些基本能力来模拟更复杂的日语句型结构,如疑问句和条件句。

疑问句体操:为什么要演奏春日影?

日语中的疑问句通常由疑问词(如なんで、なぜ、どうして)和句尾的疑问助词(如の、か)构成。观察长崎爽世女士的名言「なんで春日影やったの」,我们可以识别出其结构组成:

  1. なんで(为什么)- 疑问副词
  2. 春日影(春日影)- 句子的宾语
  3. やった(演奏了)- 动词「やる」的过去式形式
  4. の - 表示疑问的助词

这种结构在日语中非常常见,我们可以抽象出一个通用模式:[疑问词] + [主题/宾语] + [动词变形] + [疑问助词]。为了在类型系统中表示这种模式,我们需要一个专门的泛型类型 InterrogativePhrase

typescript 复制代码
// 按类别分类的疑问词
type WhyInterrogative = "なぜ" | "なんで" | "どうして";
type WhenInterrogative = "いつ";
type WhereInterrogative = "どこ";
type WhoInterrogative = "だれ" | "誰";
type WhatInterrogative = "何" | "なに";
type HowInterrogative = "どう" | "どうして";
type WhatKindInterrogative = "どんな";
type WhichInterrogative = "どれ";

// 疑问副词类型
type InterrogativeAdverb =
  | WhyInterrogative
  | WhenInterrogative
  | WhereInterrogative
  | WhoInterrogative
  | WhatInterrogative
  | HowInterrogative
  | WhatKindInterrogative
  | WhichInterrogative;

type InterrogativePhrase<
  Adv extends InterrogativeAdverb,
  Subject extends string,
  V extends Verb,
  VForm extends ConjugationForm,
  QP extends Particle = "か" // 疑问句一般默认以か为助词,可覆写
> = `${Adv}${Subject}${ConjugateVerb<V, VForm>}${QP}`;

这个泛型类型可以接收疑问副词、主题(或宾语)、动词、动词形式和疑问助词作为参数,然后将它们按照日语语法规则组合成一个完整的疑问句类型。

现在,让我们看看如何使用这个类型系统来表示「なんで春日影やったの」这句话:

typescript 复制代码
// 定义动词"やる"(做/演奏)
type やる = GodanVerb & { stem: "や"; ending: "る" };

// 定义专有名词"春日影"
type 春日影 = ProperNoun<"春日影">;

// 构建完整的疑问句
type なんで春日影やったの = InterrogativePhrase<
  WhyInterrogative, // 疑问词"なんで"(为什么)
  春日影, // 宾语"春日影"
  やる, // 动词"やる"
  "た形", // 动词以过去式形式出现
  "の" // 疑问助词"の"
>;

// 类型检查示例
const validQuestion: なんで春日影やったの = "なんで春日影やったの"; // "为什么要演奏春日影?"

通过这种方式,TypeScript 的类型系统不仅可以表示日语疑问句的结构,还能在编译时验证句子的语法正确性。这展示了类型系统能如何帮助我们学习和掌握日语语法规则。

条件句体操:辛美尔的话也会这么做的

日语中的条件句表达假设或者条件关系,有多种表达方式,如なら、たら、れば、と等。在这些条件表达中,「なら」常用于表示「如果是...的话」,通常接在名词或名词短语之后。

让我们分析芙莉莲女士的名言「ヒンメルならそうした」这个条件句:

  1. ヒンメル - 人名「辛美尔」,作为条件的主体
  2. なら - 条件助词,表示「如果是...的话」
  3. そう - 指示副词,表示「那样」
  4. した - 动词"する"(做)的过去时态

这个句子的结构可以概括为:[条件主体] + [条件助词なら] + [结果],其中结果部分是「そうした」(那样做了)。

为了在类型系统中表示这种结构,我们需要使用前面定义的几个关键类型:

typescript 复制代码
// 条件助词类型
type ConditionalParticle = "なら" | "たら" | "れば" | "と";

// 指示词类型(需要补充定义)
type Demonstrative = "こう" | "そう" | "ああ" | "どう";

// 条件句结构
type ConditionalPhrase<
  Subject extends string,
  CP extends ConditionalParticle,
  Result extends string
> = `${Subject}${CP}${Result}`;

// 指示动作组合
type DemonstrativeAction<
  Demo extends string,
  V extends Verb,
  F extends ConjugationForm = "辞書形"
> = `${Demo}${ConjugateVerb<V, F>}`;

现在,让我们使用这些类型来表示「ヒンメルならそうした」:

typescript 复制代码
// 定义专有名词"ヒンメル"
type ヒンメル = ProperNoun<"ヒンメル">;

// 定义する动词
type する = IrregularVerb & { dictionary: "する" };

// 创建そうした模式(そうする的过去形)
type そうした = DemonstrativeAction<Demonstrative & "そう", する, "た形">;

// 创建条件句"ヒンメルならそうした"
type ヒンメルならそうした = ConditionalPhrase<ヒンメル, "なら", そうした>;

// 类型检查示例
const properExample: ヒンメルならそうした = "ヒンメルならそうした"; // "辛美尔的话也会这么做的"
// const wrongExample: ヒンメルならそうした = "ヒンメルならそうする"; // 错误:动词形式不匹配

这个例子展示了如何使用类型系统精确地表示日语条件句结构,并在编译时检查句子的语法正确性。

组合句体操:好时代,来临啦!

日语中常见的一种表达方式是通过逗号连接多个简短的语句,形成一个富有节奏感的组合句。这些短句各自独立,但组合起来传达一个连贯的意思。让我们分析田所浩二先辈的名言「いいよ、来いよ」,它由三个部分组成:

  1. いいよ - 由形容词「いい」(好)和强调助词「よ」组成
  2. 日语逗号,用于分隔短句
  3. 来いよ - 由动词「来る」(来)的命令形「来い」和强调助词「よ」组成

这个句子结构可以概括为:[短句1]、[短句2],其中每个短句都可以有不同的语法结构。

为了在类型系统中表示这种组合句结构,我们可以使用以下类型:

typescript 复制代码
// 带助词的短语
type PhraseWithParticle<
  Phrase extends string,
  P extends Particle
> = `${Phrase}${P}`;

// 通过日语逗号连接的短语
type ConnectedPhrases<P1 extends string, P2 extends string> = `${P1}、${P2}`;

而对于形容词和动词的变形,只需使用前面定义的变形系统即可:

typescript 复制代码
// 定义い形容词"いい"(好的),注意它是不规则变形
type いい = IAdjective & { stem: "い"; ending: "い"; irregular: true };
type いいよ = PhraseWithParticle<ConjugateAdjective<いい, "基本形">, "よ">;

// 定义不规则动词"来る"(来)
type 来る = IrregularVerb & { dictionary: "来る" };
type 来いよ = PhraseWithParticle<ConjugateVerb<来る, "命令形">, "よ">;

// 连接两个短句 -> "いいよ、来いよ"
type いいよ来いよ = ConnectedPhrases<いいよ, 来いよ>;

// 类型检查示例
const correctPhrase1: いいよ = "いいよ"; // "真好啊!"
const correctPhrase2: 来いよ = "来いよ"; // "快来吧!"
const correctFullPhrase: いいよ来いよ = "いいよ、来いよ"; // "好时代,来临啦!"
// const incorrectPhrase: いいよ来いよ = "いいよ、くるよ"; // 错误:动词形式不匹配

这样,我们就可以用类型系统表示出由多个独立短句组成的组合句了。

总结与展望

本文通过 TypeScript 的高级类型系统探索了日语语法结构。我们从基础的动词变形开始,逐步构建了能够表示疑问句、条件句和组合句的类型系统。

传统上,这种用复杂类型系统来标注自然语言的代码编写成本极高,基本只能用来炫技。然而,现在的情况已经发生了根本性的变化------只要类型系统设计合理,LLM 已经可以为几乎任何自然语言准确地标注出 TypeScript 类型。这意味着普通人哪怕不懂 TypeScript,也能在学习语言时获得来自 LLM 的强类型提示。更有趣的是,如果你会写 TypeScript,你实际上就可以解读几乎任何一门自然语言的语法!

值得澄清的是,这个项目的范围严格限定在单向地通过 TypeScript 类型系统标注句子,而不涉及将任意字符串 parse 回语法类型------那是 LLM 更擅长的工作。所有纸质语言教材中的例句都是弱类型的,而这种方法可以为学习者提供强类型的指导。

这种借助 TypeScript 类型系统帮助学习语言的方法论,几乎可以推广到所有的自然语言。基于这一理念,我在 GitHub 上建立了 TypedGrammar 组织。目前的日语类型系统代码已经开源在 github.com/typedgramma... 。虽然目前还只是一个技术原型,但要完善到对普通人的语言学习产生实际价值,基本是一个考验耐心的 scalable 的工作。希望后续能一边实践一边分享,与感兴趣的朋友们共同学习进步。后续希望能持续分享相关进展,欢迎感兴趣的读者关注支持,一同探索类型编程与语言学习的交叉领域。

相关推荐
Mirageef4 小时前
aardio的项目文件解析
编程语言
逆袭的小黄鸭6 小时前
仿 ElementPlus 组件库(九)—— Switch 组件实现
前端·vue.js·typescript
小太阳8216 小时前
Mangojs快速上手 —— (二)处理一个请求
typescript·bun
Bonnie9 小时前
深入学习搜索树与字符匹配数据结构
算法·typescript
逆袭的小黄鸭1 天前
仿 ElementPlus 组件库(八)—— Input 组件实现
前端·vue.js·typescript
Cutey9161 天前
TypeScript 中 interface 和 type 的区别
前端·面试·typescript
Moment1 天前
面试官:为什么在 vscode 中编写 TS 代码的时候,如果类型有错能立刻检测到?
前端·javascript·typescript
丁总学Java1 天前
深入理解 JavaScript/TypeScript 中的假值(Falsy Values)与逻辑判断 ✨
开发语言·javascript·typescript
我是小七呦2 天前
🙅你真的懂enum吗,详细介绍enum的坑点
前端·typescript
leafnote2 天前
express 多语言i18n国际化,怎么做?
前端·后端·typescript