Typescript - 基于另一种类型的键约束泛型类型

时间:2021-04-06 03:47:04

标签: typescript

我正在尝试编写一个打字稿助手库来强类型 process.env,以便您可以执行以下操作。如果变量丢失或无法转换为正确类型,则带有 throw 的库。

import { getEnv, num, str } from '@lib/env'

const env = getEnv({
  TABLE_NAME: num(),
})

// typeof env is
// const env: Readonly<{
//     TABLE_NAME: number;
// }>

我目前有以下有效的代码

type Validator<T> = (
  defaultValue?: T,
) => (processEnvKey: string, processEnvValue?: string) => T

type Validators<T> = { [K in keyof T]: ReturnType<Validator<T[K]>> }

export const str: Validator<string> = (defaultValue) => (
  processEnvKey,
  processEnvValue,
) => {
  if (processEnvValue != null) return processEnvValue
  if (defaultValue != null) return defaultValue

  throw new Error(
    `Environment variable '${processEnvKey}' is required and no default was provided`,
  )
}

export const num: Validator<number> = (defaultValue) => (
  processEnvKey,
  processEnvValue,
) => {
  if (processEnvValue != null) {
    const processEnvValueAsNumber = Number(processEnvValue)
    if (Number.isNaN(processEnvValueAsNumber)) {
      throw new Error(
        `Environment variable '${processEnvKey}' is required to be numeric but could not parse '${processEnvValue}' as a number`,
      )
    }

    return processEnvValueAsNumber
  }

  if (defaultValue != null) return defaultValue

  throw new Error(
    `Environment variable '${processEnvKey}' is required and no default was provided`,
  )
}

export const getEnv = <T>(
  validators: Validators<T>,
  environment = process.env,
): Readonly<T> => {
  const result: Partial<T> = {}

  for (const processEnvKey in validators) {
    const validator = validators[processEnvKey]
    result[processEnvKey] = validator(processEnvKey, environment[processEnvKey])
  }

  return result as Readonly<T>
}

我现在有一个新要求,即我提前知道可用作接口的所有环境键

interface Env {
  API_ENDPOINT: any
  TABLE_NAME: any
}

所以我正在尝试更改 getEnv,以便传入的对象只能包含在 Env 中找到的键。

我尝试更改 getEnv 但我卡住了

export const getEnv = <T extends { [K in keyof Env]?: ????>(

如果我改成

export const getEnv = <T extends { [K in keyof Env]?: unknown>(

我可以传递额外的密钥而编译器不会抱怨。即使 NON_EXISTING_KEY 不是 Env

的键,以下内容也不会导致编译器错误
const env = getEnv({
  TABLE_NAME: num(),
  NON_EXISTING_KEY: str(),
})

1 个答案:

答案 0 :(得分:1)

好主意!

这种行为是由于 generic constraints 的设计方式造成的。 约束 T extends { [K in keyof Env]?: unknown } 强制执行 T最低要求。当然,允许任何其他属性。

您需要的是一个适当的 exact type,它可以防止定义任何多余的属性。 用户 jcalz 有一个 really great answer 来完成其中之一:

<块引用>
type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;

这需要一个类型 T 和一个 candidate 类型 U,我们希望确保“恰好是 T”。它返回一个类似于 T 的新类型,但具有额外的 never 值属性,对应于 U 中的额外属性。如果我们使用它作为 U 的约束,比如 U extends Exactly<T, U>,那么我们可以保证 U 匹配 T 并且没有额外的属性。< /p>


有了这种类型,您可以按如下方式声明您的 getEnv

export const getEnv = <T extends Exactly<{ [K in keyof Env]?: unknown }, T>>(
  validators: Validators<T>,
  environment = process.env,
): Readonly<T> => {};

因此,T 现在必须只匹配 { [K in keyof Env]?: unknown } 中的键,并且您拥有所需的限制。

Stackblitz reproduction.


PS: 由于 Validators 的定义方式,存在一种边缘情况,其中 () => never 类型的属性可以绕过约束。现在,我想不出有人会这样做的原因,但是您可以通过更新 Validators 的定义来阻止这种情况:

// from
type Validators<T> = { [K in keyof T]: ReturnType<Validator<T[K]>> }
// to
type Validators<T> = { [K in keyof T]: T[K] extends never ? never : ReturnType<Validator<T[K]>> };

如果您在第 17/18 行切换评论并刷新,则 Stackblitz 演示了这一点。