使用io-ts做配置验证时的TypeScript类型推断问题
I'm migrating from runtypes to io-ts for config validation in a new project, using this pattern:
- Create a
configTypeMapthat defines io-ts types for each config item - Generate the
Configtype from this map - Create an
envVarMapthat maps environment variables to their corresponding config keys - Write a
getConfigfunction 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

