2024-01-11 TypeScript 最烦人,也最简单的 error


TypeScript 最烦人,也最简单的 error,即:optional 声明方式不兼容。

相信所有 TypeScript 开发者都遇到下面的错误日志吧?😂

Type 'number | null' is not assignable to type 'number | undefined'.
  Type 'null' is not assignable to type 'number | undefined'.ts(2322)

虽然这个错误的解决方法很简单,但是时不时就被它烦一下,也是服气了。忍不住想吐槽几句。

TypeScript optional 声明方式不兼容造成了生态问题,不是简单的语法问题。

TJ 2024-01-11

如果只是简单的语法问题,我又何必吐槽呢?

如果只是简单的程序,没有第三方依赖,不涉及工具链,那就做好自己就行了。只要自己和团队做得好,TypeScript optional 声明方式不兼容的问题是可以避免的。

但是对于复杂的程序,有大量第三方依赖,并使用各种工具链辅助开发,那么这就是生态问题。只靠自己和团队避坑是没有用的,因为生态里的总有人会挖好坑等你。

为什么说是生态问题呢?

先解释一下我现在参加的项目的技术栈:

  1. protobuf 定义 gRPC request response,并通过 codegen 生成 gRPC server 和 client 的 TypeScript 类型定义
  2. prisma 定义 database schema,并通过 prisma generate 生成 TypeScript 类型定义
  3. class-validator 定义 validation schema
  4. TypeScript 编写业务代码和辅助数据

针对最开始分享的 TypeScript error 我分享一下我的 protobuf prisma class-validator TypeScript 分别是怎么写的,如果你是 TypeScript developer,相信你一定能看懂。

1. protobuf and generated code

protobuf code

// .proto
message ChargingProfile {
  // ...
  optional int32 duration = 2;
  // ...
}

protobuf generated code

// dist/buf/check_in_history_pb.ts
export class ChargingProfile extends Message<ChargingProfile> {
  // ...
  /**
   * @generated from field: optional int32 duration = 2;
   */
  duration?: number;
  // ...

2. prisma and generated code

prisma schema

// schema.prisma
model ChargingProfile {
  // ...
  duration                Int?
  // ...
}

prisma schema generated code

// node_module/.prisma/client/index.d.ts
export type ChargingProfile = {
  // ...
  duration: number | null
  // ...
}

小结

注意看,protobuf generated code 和 prisma schema generated code

  // protobuf generated code
+  duration?: number;
  //prisma schema generated code
+  duration: number | null

最开始分享的 TypeScript error 就是因为这个原因造成的,我在 x.com 上分享了这段代码,很多朋友分享了他们的避免方法。但是 generated code 完全不是我编写的,而是 TypeScript 生态中工具链生成的,即使我的定义是相同的,不同工具链生成的代码还是造成了 TypeScripr error,我真不知道我要如何避免这个错误。🤷

BTW 这个问题的解决方法非常简单,我并不是不能解决。 只是它经常出来烦我一下,让我忍不住想吐槽。

TJ 2024-01-11

3. class-validator

export class ChargingProfileDto {
  // ...
  @IsOptional()
  @IsInt()
  duration?: number;
  // ...
}

其实 class-validator 实现的代码和本文没什么关系,不过这段代码是真实存在于我们的代码库中的,我就顺便分享一下。

并且,我非常不喜欢 class-validator,原因有以下几点:

  1. class-validator 和 TypeScript 在某些方面是重复的,例如 @IsOptional()?
  2. class-validator 只能校验 class instance,如果想校验 JSON 还需要额外依赖(class-transformer)
  3. class-validator 文档不全,例如:我一直没有找到 @IsNumber(options: IsNumberOptions) 中 options 有哪些配置项

最后,分享几种 TypeScript 中 number 的定义

type A = {
  a?: number
}

type B = {
  a: number | undefined
}

type C = {
  a: number | null
}

type D = {
  a: number | null | undefined
}

type E = {
  a: number = 0
}

大家参加的项目中用哪一种呢?就个人而言,你倾向哪一种?

欢迎大家和我一起讨论:

other refs