'{ host: string | 类型的参数不明确的;用户:字符串 | undefined ... }' 不可分配给 'string | ConnectionConfig' 类型的参数

Argument of type '{ host: string | undefined; user: string | undefined ... }' is not assignable to parameter of type 'string | ConnectionConfig'

我正在创建与 AWS mysql 数据库的连接,如下所示:

const config = {
  host: process.env.RDS_HOSTNAME,
  user: process.env.RDS_USERNAME,
  password: process.env.RDS_PASSWORD,
  port: 3306,
  database: process.env.RDS_DB_NAME,
}

const db = mysql.createConnection(config) // config gets highlighted

但我收到以下错误:

Argument of type '{ host: string | undefined; user: string | undefined; password: string | undefined; port: number; database: string | undefined; }' is not assignable to parameter of type 'string | ConnectionConfig'.

Type '{ host: string | undefined; user: string | undefined; password: string | undefined; port: number; database: string | undefined; }' is not assignable to type 'ConnectionConfig'.

    Types of property 'host' are incompatible.
      Type 'string | undefined' is not assignable to type 'string'.
        Type 'undefined' is not assignable to type 'string'.ts(2345)

早些时候,我遇到来自 .env 的端口问题。当我切换到对端口进行硬编码时,我得到了这个。

我不明白问题是什么,也不知道如何解决。

问题是 process.env@types/node 中声明为:

// process.d.ts
   ...
   interface ProcessEnv extends Dict<string> {}
   ...
   env: ProcessEnv

// global.d.ts
    interface Dict<T> {
        [key: string]: T | undefined;
    }
    ...  

您可能会注意到 env 中的任何查找结果都是 string | undefined。虽然 createConnection 预计 string 至少 host 属性.

您有多种选择来确保通过 config 的编译器正常:

  • 如果您完全确定所有环境变量都已正确设置,只需对您的配置进行类型转换:
type NoUndefinedField<T> = {
  [P in keyof T]: Exclude<T[P], undefined>;
};

createConnection(config as NoUndefinedField<typeof config>)

更新

快速解释:

这里我们使用 <T> generics to abstract over concrete type, mapped type [P in keyof T] to iterate through all properties ([P in keyof T]) of type T and built-in utility type Exclude<> to remove undesired types from each property of type T[K]。 在我们从 typeof config we typecast config 值的属性中删除所有 undefined 类型到此类型之后。

分步说明:

所以,我们有一个对象(值)config,类型为:

type ConfigUndefined = {
  host: string | undefined,
  username: string | undefined,
  port: number,
}

declare const config: ConfigUndefined

但我们需要一个 mysql create 方法可以消化的另一种类型的对象:

type Config = { // defined somewhere inside `mysql` library
  host: string,
  username: string,
  port: number,
}

确保编译器我们 更好地了解我们的值在运行时的类型的最快方法是type assertion。我们从 mysql 库中导入 Config 类型,并断言编译器我们的 config 对象具有与预期完全相同的类型:

mysql.createConnection(config as Config)

playground link

到目前为止一切顺利。 createConnection 满意了,可以到此为止了。尽管这种方法很脆弱。如果 mysql 的库维护者稍后将另一个必需的 属性 添加到 Config 类型怎么办?我们的代码仍然断言编译器我们的 config: ConfigUndefined 值具有 绝对 Config 相同的类型,但实际上它不再是

declare function createConnection(config: Config): void
declare const config: ConfigUndefined

type ConfigUndefined = {
    host: string | undefined,
    username: string | undefined,
    port: number
}

type Config = {
    host: string,
    username: string,
    port: number,
    smthing: string
}

createConnection(config as Config) // ok. compiler is stil happy

playground link

好吧,在运行时错误之后我们可以调查问题,将 smthing: process.env.SMTHING 添加到我们的 config 对象中,我们又好了。下次见。

如果我们在编译时得到那个错误不是更好吗?甚至没有 运行 我们的代码?所以我们不能直接依赖mysql的Config类型。我们应该让编译器检查我们的真实类型是否可分配(兼容) Config 。为了让它工作,我们应该让编译器计算你的 config 对象类型 的确切形状,而不需要每个 属性 中那些讨厌的 undefined 类型.

我们应该以某种方式遍历 ConfigUndefined 类型的每个 属性 并删除 undefined 类型(如果有的话)。

让我们从一个简单的类型开始。 string | undefinedunion type. To remove undefined from string | undefined type we can use conditional types and namely their distribution 属性 为:

type StringOrUndefined = string | undefined
type RemoveUndefined<T> = T extends undefined ? never : T

type StringWoUndefined = RemoveUndefined<StringOrUndefined>
// type StringWoUndefined ~ string

幸运的是,我们在标准库中有 Exclude 实用程序类型,它对我们想要排除的任意类型完全相同。我们可以将 StringWoUndefined 类型重写为:

type StringOrUndefined = string | undefined
type StringWoUndefined = Exclude<StringOrUndefined, undefined>

现在我们必须遍历 ConfigUndefined 个属性并从每个属性中删除 undefined。这就是 mapped types and keyof 运算符派上用场的地方:

type ConfigWoUndefined = {
  // keyof ConfigUndefined = "host" | "username" | "port"
  [K in keyof ConfigUndefined]: Exclude<ConfigUndefined[K], undefined>
}
// type ConfigWoUndefined = {
//   host: string,
//   username: string,
//   port: number,
// }

此处对于 "host" | "username" | "port" 中的每个 K 打字稿计算 ConfigUndefined[K] indexed access type 并在 Exclude 实用程序类型的帮助下删除 undefined 类型来自它。

尽管该类型看起来非常有用,如果我们想重用相同的功能,我们将不得不再次重新输入它,仅将 ConfigUndefined 更改为我们想要删除的其他类型 undefineds从。 generic types 看起来不错。这是一种类型级别的函数,它接受类型参数和 return 另一个修改后的类型:

// our `old` ConfigWoUndefined
// type ConfigWoUndefined = {
//   [K in keyof ConfigUndefined]: Exclude<ConfigUndefined[K], undefined>
// }

type NoUndefinedField<T> = {
  [K in keyof T]: Exclude<T[K], undefined>
}

type ConfigWoUndefined = NoUndefinedField<ConfigUndefined>

到目前为止一切顺利。但是我们仍然有一个不同的对象(值)config 和类型 ConfigUndefined。我们必须让它们保持同步。这就是 typeof 运算符的用途:

declare const process: { env: { [k: string]: string | undefined } }

const config = {
  host: process.env.HOST,
  username: process.env.USER,
  port: 3000,
}

type A = typeof config
// type A = {
//   host: string | undefined,
//   username: string | undefined,
//   port: number,
// }

我们终于可以摆脱 ConfigUndefined 类型了。

所以总结一下:

createConnection(config as NoUndefinedField<typeof config>)

在这里,我们使用 typeof config 获取值 config 的确切类型,从其每个属性中删除 undefined 使用 NonUndefinedField 类型并强制转换我们的 config 值到此修改后的类型。这使得打字稿检查 NoUndefinedField<typeof config> 是否可分配给 mysql 的 Config 预期类型。

如果我们绝对确定所有必需的 env 都按应有的方式定义,那么这可能是有道理的。更好的做法仍然是使用运行时检查 narrow 类型。就像下面在断言函数中所做的那样。


function assertConfig<T>(config: T): asserts config is NoUndefinedField<T> {
  for (const key in config) {
    if (typeof config[key] === 'undefined') 
      throw new Error(`Config check failed. Key "${key}" is undefined.`)
  }
}

playground link

不过,我相信生产代码断言功能是在数据库配置错误时提前退出的正确方法。