Why TypeScript is a better option than JavaScript when it comes to functional programming?

2025-06-10

为什么在函数式编程方面,TypeScript 是比 JavaScript 更好的选择?

在这篇文章中,我想讨论静态类型在函数式编程语言中的重要性,以及为什么由于 JavaScript 缺乏静态类型系统,TypeScript 在函数式编程方面比 JavaScript 更好。

绘画

函数式编程代码库中没有类型的生活

请尝试设想一个假设的情况,以便我们展示静态类型的价值。假设您正在为一个与选举相关的应用程序编写代码。您刚刚加入团队,这个应用程序规模很大。您需要编写一个新功能,其中一项要求是确保应用程序的用户有资格在选举中投票。团队中的一位老成员告诉我们,我们需要的部分代码已经在一个名为 的模块中实现@domain/elections,我们可以按如下方式导入它:

import { isEligibleToVote } from "@domain/elections";
Enter fullscreen mode Exit fullscreen mode

导入是一个很好的起点,我们非常感谢同事提供的帮助。是时候完成一些工作了。但是,我们遇到了一个问题。我们不知道如何使用isEligibleToVote。如果我们尝试通过名称猜测的类型isEligibleToVote,我们可能会认为它很可能是一个函数,但我们不知道应该为其提供什么参数:

isEligibleToVote(????);
Enter fullscreen mode Exit fullscreen mode

我们不怕阅读别人的代码,我们打开模块源代码的源代码@domain/elections,我们会遇到以下情况:

const either = (f, g) => arg => f(arg) || g(arg);
const both = (f, g) => arg => f(arg) && g(arg);
const OUR_COUNTRY = "Ireland";
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = person => Boolean(person.naturalizationDate);
const isOver18 = person => person.age >= 18;
const isCitizen = either(wasBornInCountry, wasNaturalized);
export const isEligibleToVote = both(isOver18, isCitizen);
Enter fullscreen mode Exit fullscreen mode

上述代码片段采用了函数式编程风格。它isEligibleToVote执行了一系列检查:

  • 该人必须年满 10 岁
  • 此人必须是公民
  • 要成为公民,该人必须在该国出生或入籍

我们需要在脑子里做一些逆向工程才能解码上面的代码。我几乎确定这isEligibleToVote是一个函数,但现在我有些怀疑,因为我没有在它的声明中看到function关键字或箭头函数 ( ):=>

const isEligibleToVote = both(isOver18, isCitizen);
Enter fullscreen mode Exit fullscreen mode

为了弄清楚它是什么,我们需要检查both函数的功能。我可以看到,它们都接受两个参数f和,g并且我可以看到它们是函数,因为它们被调用了f(arg)g(arg)。该both函数返回一个arg => f(arg) && g(arg)接受名为的参数的函数args,其形状目前我们完全不知道:

const both = (f, g) => arg => f(arg) && g(arg);
Enter fullscreen mode Exit fullscreen mode

现在我们可以返回isEligibleToVote函数并再次尝试检查,看看能否发现一些新的东西。我们现在知道了 是函数isEligibleToVote返回的函数,我们也知道,所以正在执行类似于以下内容的操作:botharg => f(arg) && g(arg)fisOver18gisCitizenisEligibleToVote

const isEligibleToVote = arg => isOver18(arg) && isCitizen(arg);
Enter fullscreen mode Exit fullscreen mode

我们仍然需要找出参数是什么arg。我们可以检查isOver18isCitizen函数来找到一些细节。

const isOver18 = person => person.age >= 18;
Enter fullscreen mode Exit fullscreen mode

这条信息很有帮助。现在我们知道了isOver18需要一个名为 的参数person,并且它是一个带有名为 的属性的对象,age通过比较,我们还能够猜出person.age >= 18age是一个数字。

让我们看isCitizen一下该函数:

const isCitizen = either(wasBornInCountry, wasNaturalized);

Enter fullscreen mode Exit fullscreen mode

我们在这里运气不佳,我们需要检查eitherwasBornInCountrywasNaturalized功能:

const either = (f, g) => arg => f(arg) || g(arg);
const OUR_COUNTRY = "Ireland";
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = person => Boolean(person.naturalizationDate);
Enter fullscreen mode Exit fullscreen mode

和都wasBornInCountry期望wasNaturalized一个名为的参数person,现在我们发现了新的属性:

  • birthCountry属性似乎是一个字符串
  • naturalizationDate属性似乎为日期或空

either函数将参数传递给 和wasBornInCountrywasNaturalized这意味着arg必须是一个人。这花费了我们大量的认知精力,我们感到很累,但现在我们知道可以isElegibleToVote按如下方式使用该函数:

isEligibleToVote({
    age: 27,
    birthCountry: "Ireland",
    naturalizationDate: null
});

Enter fullscreen mode Exit fullscreen mode

我们可以使用 JSDoc 等文档来解决其中的一些问题。然而,这意味着更多的工作,而且文档很快就会过时。

TypeScript 可以帮助验证我们的 JSDoc 注释是否与代码库保持同步。但是,如果我们要这样做,为什么不一开始就采用 TypeScript 呢?

函数式编程代码库中的类型生活

现在我们知道了在没有类型的函数式编程代码库中工作有多么困难,我们将看看在具有静态类型的函数式编程代码库上工作的感觉。我们将回到同样的起点,我们加入了一家公司,我们的一位同事向我们介绍了这个@domain/elections模块。然而,这一次我们身处一个平行宇宙,代码库是静态类型的。

import { isEligibleToVote } from "@domain/elections";
Enter fullscreen mode Exit fullscreen mode

我们不知道它isEligibleToVote是不是函数。不过,这次我们可以做的远不止猜测。我们可以使用 IDE 将鼠标悬停在isEligibleToVote变量上,以确认它是一个函数:

然后我们可以尝试调用该isEligibleToVote函数,我们的 IDE 会让我们知道需要传递一个类型的对象Person作为参数:

如果我们尝试传递一个对象文字,我们的 IDE 将显示该类型的所有属性及其Person类型:

就是这样!无需思考或记录!这一切都归功于 TypeScript 类型系统。

以下代码片段包含模块的类型安全版本@domain/elections

interface Person {
    birthCountry: string;
    naturalizationDate: Date | null;
    age: number;
}

const either = <T1>(
   f: (a: T1) => boolean,
   g: (a: T1) => boolean
) => (arg: T1) => f(arg) || g(arg);

const both = <T1>(
   f: (a: T1) => boolean,
   g: (a: T1) => boolean
) => (arg: T1) => f(arg) && g(arg);

const OUR_COUNTRY = "Ireland";
const wasBornInCountry = (person: Person) => person.birthCountry === OUR_COUNTRY;
const wasNaturalized = (person: Person) => Boolean(person.naturalizationDate);
const isOver18 = (person: Person) => person.age >= 18;
const isCitizen = either(wasBornInCountry, wasNaturalized);
export const isEligibleToVote = both(isOver18, isCitizen);
Enter fullscreen mode Exit fullscreen mode

添加类型注解可能需要一些额外的类型,但其好处无疑是值得的。我们的代码将更不容易出错,并且能够自文档化,团队成员的工作效率也会更高,因为他们将花费更少的时间来理解已有的代码。

通用的用户体验原则“ Don't Make Me Think”也能极大地提升我们的代码质量。记住,我们花在阅读代码上的时间远比写代码的时间多得多。

关于函数式编程语言中的类型

函数式编程语言不一定是静态类型的。然而,函数式编程语言往往倾向于静态类型。根据维基百科,这种趋势自 20 世纪 70 年代以来就一直盛行:

自 1970 年代 Hindley–Milner 类型推断的出现以来,函数式编程语言倾向于使用类型化的 lambda 演算,在编译时拒绝所有无效程序,并冒着出现假阳性(false positive)的风险。这与 Lisp 及其变体(例如 Scheme)中使用的无类型 lambda 演算相反,后者在编译时接受所有有效程序,并冒着出现假阴性(false negative)的风险。Lisp 及其变体(例如 Scheme)中使用的无类型 lambda演算在运行时拒绝所有无效程序,前提是信息足以保证不会拒绝有效程序。代数数据类型的使用使得复杂数据结构的操作变得方便;强编译时类型检查的存在使得程序在缺乏其他可靠性技术(例如测试驱动开发)的情况下更加可靠,而类型推断使程序员在大多数情况下无需手动向编译器声明类型。

让我们考虑一下没有类型的面向对象isEligibleToVote特性的实现:

const OUR_COUNTRY = "Ireland";

export class Person {
    constructor(birthCountry, age, naturalizationDate) {
        this._birthCountry = birthCountry;
        this._age = age;
        this._naturalizationDate = naturalizationDate;
    }
    _wasBornInCountry() {
        return this._birthCountry === OUR_COUNTRY;
    }
    _wasNaturalized() {
        return Boolean(this._naturalizationDate);
    }
    _isOver18() {
        return this._age >= 18;
    }
    _isCitizen() {
        return this._wasBornInCountry() || this._wasNaturalized();
    }
    isEligibleToVote() {
        return this._isOver18() && this._isCitizen();
    }
}
Enter fullscreen mode Exit fullscreen mode

弄清楚如何调用前面的代码并不是一个简单的任务:

import { Person } from "@domain/elections";

new Person("Ireland", 27, null).isEligibleToVote();
Enter fullscreen mode Exit fullscreen mode

再一次,由于没有类型,我们被迫查看实现细节。

constructor(birthCountry, age, naturalizationDate) {
    this._birthCountry = birthCountry;
    this._age = age;
    this._naturalizationDate = naturalizationDate;
}
Enter fullscreen mode Exit fullscreen mode

当我们使用静态类型时事情变得更容易:

const OUR_COUNTRY = "Ireland";

class Person {

    private readonly _birthCountry: string;
    private readonly _naturalizationDate: Date | null;
    private readonly _age: number;

    public constructor(
        birthCountry: string,
        age: number,
        naturalizationDate: Date | null
    ) {
        this._birthCountry = birthCountry;
        this._age = age;
        this._naturalizationDate = naturalizationDate;
    }

    private _wasBornInCountry() {
        return this._birthCountry === OUR_COUNTRY;
    }

    private _wasNaturalized() {
        return Boolean(this._naturalizationDate);
    }

    private _isOver18() {
        return this._age >= 18;
    }

    private _isCitizen() {
        return this._wasBornInCountry() || this._wasNaturalized();
    }

    public isEligibleToVote() {
        return this._isOver18() && this._isCitizen();
    }

}
Enter fullscreen mode Exit fullscreen mode

构造函数告诉我们需要多少个参数以及每个参数的预期类型:

public constructor(
    birthCountry: string,
    age: number,
    naturalizationDate: Date | null
) {
    this._birthCountry = birthCountry;
    this._age = age;
    this._naturalizationDate = naturalizationDate;
}
Enter fullscreen mode Exit fullscreen mode

我个人认为函数式编程通常比面向对象编程更难逆向。这或许是因为我的面向对象背景。然而,无论原因如何,我确信一件事:类型确实让我的工作更轻松,而且当我在处理函数式编程代码库时,它们的好处更加明显。

概括

静态类型是宝贵的信息来源。由于我们花在阅读代码上的时间远多于编写代码的时间,我们应该优化工作流程,以便更高效地阅读代码,而不是更高效地编写代码。类型可以帮助我们减少大量的认知投入,从而专注于我们试图解决的业务问题。

虽然所有这些在面向对象编程代码库中都是正确的,但在函数式编程代码库中,其优势更加明显,这正是我认为 TypeScript 在函数式编程方面比 JavaScript 更好的原因。你怎么看?

如果你喜欢这篇文章,并且对函数式编程或 TypeScript 感兴趣,请查看我即将出版的新书《使用 TypeScript 进行函数式编程》。

鏂囩珷鏉由簮锛�https://dev.to/wolksoftware/why-typescript-is-a-better-option-than-javascript-when-it-comes-to- functional-programming-3mp0
PREV
什么是 API?
NEXT
React 和 TypeScript 的三大陷阱