The common misconception about TypeScript

2025-06-10

关于 TypeScript 的常见误解

最近我的朋友开始学习TypeScript,昨天他来找我问一个问题,他遇到了一个问题,搞了好几天都没搞明白。(他虽然是个新手,但求知欲很强。)他的疑问让我意识到有些人对 TypeScript 存在一个常见的误解。所以我写了这篇文章,解释一下导致这种误解的原因以及如何纠正。请注意:这篇文章是写给新手和有志于学习 JavaScript 的开发者的,经验丰富的开发者会觉得这很正常。

简要概述:

我的朋友实际上正在尝试使用React和 TypeScript构建一个前端。我可能不会透露他想要实现的具体细节,但我尝试给出一个类似的例子。他的设置包括一个用Express.js编写的 Web 服务器,其中包含一些 API。前端代码的一部分向 API 发出 GET 请求,并接收 JSON 格式的响应,然后它会处理内容以在网页上显示结果。我将尝试通过编写两个脚本文件来模拟这样的环境。

  1. 在模拟中,Web 服务器代码将有一个虚拟端点,并在请求时返回一个虚拟 JSON 对象。
  2. 前端代码,在我的情况下只是一个脚本,它发出 HTTP GET 请求并获取对象,然后对该对象执行一个简单的操作,控制台记录结果,用 TypeScript 编写,我将使用官方的 type-script 编译器将其编译为 JavaScript。

服务器代码:(server.js

const express = require('express')

app = express()

app.get("/dummy", (req, resp) => {
    return resp.status(200).json({
        dummyValue : 121
    })
})

app.listen(6000, () => {
    console.log("Running server")
})
Enter fullscreen mode Exit fullscreen mode

这段代码看起来很简单,应该清楚的是服务器返回一个dummyValue用一些随机值调用的整数字段。

客户:(client.ts

import axios, { AxiosResponse } from 'axios'


interface DummyResponse {
    dummyValue: number;
}

const generateResult = (response: DummyResponse): number => {
    return response.dummyValue + 1
}

const makeRequest = async (url: string) => {
    try {
        const response: AxiosResponse<DummyResponse> = await axios.get(url)

        if (response.status !== 200) {
            throw `Got response ${response.status}`
        }

        if (!response.data) {
            throw "No data in the response"
        }

        const respJson: DummyResponse = response.data
        const result: number = generateResult(respJson)

        console.log(`Result : ${result}`)

    } catch (err) {
        console.log(`Failed to get response err string = ${err}`)
    }
}

makeRequest('http://localhost:6000/dummy')
Enter fullscreen mode Exit fullscreen mode

客户端代码是用 TypeScript 编写的,脚本用于axios发起 HTTP GET 请求,并在必要时清晰地定义接口和类型。generateResult函数接受响应对象并加dummyValue1,然后直接返回值。如果你想复现,也可以使用这个package.json 文件:

{
  "name": "client",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "build-client": "tsc client.ts",
    "test-client": "node client.js",
    "start-server": "node server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/node": "^14.14.35",
    "axios": "^0.21.1",
    "express": "^4.17.1",
    "typescript": "^4.2.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

我在这里定义了三个脚本命令。使用build-client构建 JavaScript 文件。将使用本地 node.js 环境运行生成的文件。将启动使用编写的 Web 服务器client.jsclient.tstsctest-clientclient.jsstart-serverexpress.js

要编译并开始,您可以将这三个文件复制到本地,然后运行以下命令:

npm i
npm run build-client
Enter fullscreen mode Exit fullscreen mode

问题:

现在,让我们运行服务器并测试客户端脚本。
运行服务器的步骤如下:

npm run start-server
Enter fullscreen mode Exit fullscreen mode

接下来,在另一个终端中运行客户端:

npm run test-client
Enter fullscreen mode Exit fullscreen mode

客户端按预期产生以下输出:

Result : 122
Enter fullscreen mode Exit fullscreen mode

这很好,客户端只是做了它预期要做的事情,它向发出请求http://localhost:6000/dummy并得到结果{'dummyValue' : 121},然后它将 1 加到dummyValue,所以结果就是 122。

现在我们将稍微修改一下服务器端代码,但不会修改客户端,我们重用相同的已编译 JavaScript client.js。让我们像这样修改服务器代码:

const express = require('express')

app = express()

app.get("/dummy", (req, resp) => {
    return resp.status(200).json({
        dummyValue : "121"  //dummyValue has a string value now.
    })
})

app.listen(6000, () => {
    console.log("Running server")
})
Enter fullscreen mode Exit fullscreen mode

我们做了一个非常简单的更改,只是将其设置dummyValue为包含字符串值而不是数字类型。我们重新启动服务器并再次运行相同的客户端代码,但得到了以下输出:

Result : 1211
Enter fullscreen mode Exit fullscreen mode

我们得到了一个输出,没有任何错误,但这正确吗?当然不正确!这差别太大了,我们得到的不是 122,而是 1211。想象一下,这个错误的结果会给后续计算带来多大的混乱,或者想象一下,如果这是一个血压监测系统或与医疗保健相关的系统,这会造成多大的破坏!哈哈。构建前端的开发人员除非定期检查应用程序,否则在部署后不会意识到这一点。

我的朋友没能解决这个问题,因为代码仍然运行良好,没有返回任何错误或警告。但他注意到这个问题后就来找我了。(记住,他是 JavaScript 新手)

记住这一点,让我们深入研究这些概念。

人们为什么使用 TypeScript?

首先,您必须理解为什么在使用 JavaScript 编码足以满足大多数用例的情况下我们需要 TypeScript。人们使用 TypeScript 来实现编译时类型安全。根据微软的定义,TypeScript 是 JavaScript 的超集,并提供了原始 JavaScript 中没有的许多内置功能。编译时类型检查是主要功能之一。通常,TypeScript 是 JavaScript 的扩展,其中每个符号都有固定/静态类型。JavaScript 不关心类型,变量/常量可以采用任何值,并且可以随时更改它们(对于变量)。所以 JavaScript 是一种untyped语言。人们会发现像 JavaScript 这样的无类型语言更容易使用,但当代码库变大时,真正的问题就出现了,您可能最终无法再跟踪您使用的变量和每个变量的类型,因为您必须自己跟踪类型安全以维护结果的完整性并避免不必要的错误。

TypeScript 解决了这个问题,它为你使用的每个符号绑定一个静态类型,并自行跟踪赋值。由于 TypeScript 会为你完成这项工作,你无需自己动手。因此,TypeScript 使大型代码库更易于维护,并可在开发人员和团队之间共享。但是等等!还有一个问题。

编译时与运行时类型检查:

尽管 TypeScript 是 JavaScript 的超集,但它无法独立运行。它只是一个 JavaScript 生成器。最终,TypeScript 代码会被编译为纯 JavaScript。由 TypeScript 生成的 JavaScript 可以在任何 JavaScript 实现上执行。在编译期间,TypeScript 编译器会检查类型不匹配并报告此类错误。例如,一个数字只能与另一个数字相加,而不能将字符串与数字相加。由于这些问题在编译时报告,开发人员可以确保生产环境中不会因类型混合而出现错误,这是一件好事!

这并不意味着此类问题永远不会再发生。因为类型安全只会在编译期间评估一次,之后就永远不会再发生。TypeScript 生成的 JavaScript 代码不会嵌入任何类型相关的信息,因为 JavaScript 本身并不关心类型。换句话说,在运行时,你的应用程序仍然不是类型安全的。

处理动态数据的问题(没有固定的模式/类型)

由于 TypeScript 仅保证编译时安全,因此编译时没有错误并不意味着您的应用程序永远不会崩溃。在编译时没有动态性,这意味着您假设一种固定的数据类型并对其进行处理。想象一下,这些数据来自外部源(来自外部 API / 服务),那么数据是动态的,其结构或类型可以随时更改,但是您使用 TypeScript 编写和部署的应用程序不会考虑到这一点,因为在运行时,您的 TypeScript 应用程序作为普通的无类型 JavaScript 存在。在大多数情况下,类型转换是自动的,并按照 JavaScript 实现定义的原则工作,例如,当一个数字与另一个字符串相加时,它会被强制转换为字符串,这是静默发生的,没有任何通知或异常。这是 JavaScript 中最常见的错误类型,因为许多网站都会处理来自外部/第三方 API 的不可预测和动态数据。

在我考虑的示例中,客户端代码通过接口静态定义了 API 响应的类型DummyResponse,该接口假定dummyValue键为数字类型,因此该函数generateOutput能够在dummyValue没有任何编译时错误的情况下将 加 1,因为加法的两个值属于同一类型。然而,在第二种情况下, 的类型dummyValue在服务器端更改为字符串,但客户端并未意识到这一变化,尽管这违反了 TypeScript 的原则,但该错误被忽略了,因为运行时 JavaScript 看到了动态输出并执行了操作,而无需考虑 的类型dummyValue

在任何强类型语言中都不会出现这种情况,因为这些语言至少会抛出运行时异常或错误。(例如 Go 和 Java 等语言)

这真的是 TypeScript 的问题吗?

不是,因为 TypeScript从未隐式承诺过运行时类型检查。人们经常误解这一点,并假设 TypeScript 同时提供运行时和编译时类型安全。Type脚本编译完成后,“a”的概念就消失了。如果您熟悉 Python,可以将 TypeScript 与Python 的类型系统进行比较,这些工具旨在帮助开发人员在开发过程中摆脱错误和麻烦,但许多人认为它可以处理两种类型检查的情况。这是由于知识差距造成的,不了解类型或类型检查的开发人员可能无法理解 TypeScript 的这一限制,并忽略对动态数据进行显式类型检查。

如何避免这个问题?

这个问题的解决方案很简单,进行明确的类型检查,我可以修改generateOutput函数以包含明确的类型检查,如下所示:

const generateResult = (response: DummyResponse): number => {
    try {
        if (typeof response.dummyValue !== "number") {
            throw `Improper type of dummyValue, expected number, got ${typeof response.dummyValue}`
        }
        return response.dummyValue + 1

    } catch (err) {
        console.log(`Failed to generate result, error = ${err}`)
        return NaN
    }
}
Enter fullscreen mode Exit fullscreen mode

该函数执行类型检查,如果条件不满足则抛出异常。执行显式类型检查的方法多达数百种。如果您不想自己编写代码进行类型检查,或者需要处理包含许多嵌套对象的复杂数据,那么可以考虑使用npm中一些流行的验证库。我在这里尝试列出其中几种:

  1. 物联网
  2. 验证器.js
  3. 检查类型
  4. 类型检查

这些库可以使用简单的模式定义对复​​杂对象执行验证。您还可以了解Mongoose ODM如何针对 MongoDB 数据进行模式验证,并遵循类似的模式结构。

避免在前端进行明确的类型检查:

没有合适的方法可以完全摆脱类型验证,因为 JavaScript 不会隐式执行类型检查,但你可以通过更改应用程序架构来在一定程度上避免它,以下是一些提示:

  1. 实现动态模式验证系统,让前端获取后端模式并相应地调整其验证流程,这样可以避免在多个地方更改模式。查看 [ https://json-ld.org/ ] 了解类似的类比。
  2. 不要直接从外部/第三方 API 获取数据,构建一个充当代理的后端服务,并在后端实现验证功能。该后端还可以过滤掉一些前端不需要的字段。这样可以保持前端的整洁,并将所有复杂的事务都处理在后端,这也是一种良好的安全实践。请查看依赖倒置原则
  3. 考虑使用GraphQL,因为它在内部执行验证。
  4. 您还可以考虑Protobuf + gRPC而不是 HTTP/s + REST。

感谢阅读!

鏂囩珷鏉ユ簮锛�https://dev.to/narasimha1997/the-most-common-misconception-about-typescript-3f2a
PREV
使用 Tailwind CSS 设置 create-react-app
NEXT
作为 Vue 开发人员,您是否犯了这些错误?