You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用io-ts做配置验证时的TypeScript类型推断问题

How to preserve type context with io-ts when decoding config items by key?

I'm migrating from runtypes to io-ts for config validation in a new project, using this pattern:

  1. Create a configTypeMap that defines io-ts types for each config item
  2. Generate the Config type from this map
  3. Create an envVarMap that maps environment variables to their corresponding config keys
  4. Write a getConfig function that fetches and validates config values by key

This worked perfectly with runtypes, but with io-ts, the return type of configTypeMap[key].decode gets inferred as Left<t.Errors> | Right<string> | Right<{ user: string; password: string; host: string; port: string; database: string; }>—it loses the context of which specific key we're working with. I can cast the result to Either<t.Errors, Config[T]>, but I want to validate the logic without relying on type assertions.

I've confirmed this is a TypeScript inference issue, and I still want to implement a function with the signature <T extends keyof Config>(key: T): Config[T].

Example Code

Here's a simplified version of my setup:

import * as t from 'io-ts';

// Define io-ts decoders for each config entry
const configTypeMap = {
  API_URL: t.string,
  DB_CONFIG: t.type({
    user: t.string,
    password: t.string,
    host: t.string,
    port: t.string,
    database: t.string,
  }),
} as const;

// Derive the Config type from our decoder map
type Config = {
  [K in keyof typeof configTypeMap]: t.TypeOf<typeof configTypeMap[K]>;
};

// Map raw environment values to config keys
const envVarMap = {
  API_URL: process.env.API_URL,
  DB_CONFIG: JSON.parse(process.env.DB_CONFIG || '{}'),
} as const;

// The problematic function
function getConfig<T extends keyof Config>(key: T) {
  const decoder = configTypeMap[key];
  const rawValue = envVarMap[key];
  const result = decoder.decode(rawValue);

  // Result is inferred as a union of all possible Right types, not Config[T]
  if (result._tag === 'Right') {
    // Currently forced to cast here
    return result.right as Config[T];
  } else {
    throw new Error(`Invalid config for ${key}: ${JSON.stringify(result.left)}`);
  }
}

// Usage: Without the cast, apiUrl would be string | DBConfig type
const apiUrl = getConfig('API_URL');
const dbConfig = getConfig('DB_CONFIG');

Solutions to Preserve Type Context

1. Explicitly Assert the Decoder Type

Since we know configTypeMap[key] exactly matches the decoder for Config[T], we can safely assert the decoder's type to help TypeScript infer correctly:

function getConfig<T extends keyof Config>(key: T) {
  // Explicitly type the decoder as t.Type<Config[T]>
  const decoder = configTypeMap[key] as t.Type<Config[T]>;
  const rawValue = envVarMap[key];
  const result = decoder.decode(rawValue);

  if (result._tag === 'Right') {
    // No cast needed anymore—result.right is correctly inferred as Config[T]
    return result.right;
  } else {
    throw new Error(`Invalid config for ${key}: ${JSON.stringify(result.left)}`);
  }
}

This assertion is type-safe because our configTypeMap and Config type are tightly coupled.

2. Extract Decoding Logic to a Generic Helper

Another clean approach is to wrap the decoding logic in a generic helper function, which helps TypeScript track the type relationship between the decoder and the returned value:

// Generic helper to decode and throw on failure
function decodeValue<T>(decoder: t.Type<T>, value: unknown): T {
  const result = decoder.decode(value);
  if (result._tag === 'Right') {
    return result.right;
  } else {
    throw new Error(`Invalid value: ${JSON.stringify(result.left)}`);
  }
}

// Simplified getConfig with proper type inference
function getConfig<T extends keyof Config>(key: T) {
  return decodeValue(configTypeMap[key], envVarMap[key]);
}

This way, TypeScript automatically infers that decodeValue returns Config[T] because configTypeMap[key] is a decoder for that type.

Why This Happens

TypeScript's index access on union types tends to flatten all possible return types into a single union, rather than preserving the specific type for the current key. When you access configTypeMap[key], TypeScript sees it as a union of all decoders in the map, so decode returns a union of all possible Right types. By explicitly tying the decoder to Config[T] (either via assertion or a generic helper), we help TypeScript narrow down the type to the one relevant for the key passed in.

内容的提问来源于stack exchange,提问作者Sam Schick

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 14:12:48