我正在尝试编写一个打字稿助手库来强类型 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(),
})
答案 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 }
中的键,并且您拥有所需的限制。
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 演示了这一点。