Update
Creating a mapping function partialRecord
(leveraging io-ts
's peer dep on fp-ts
), we can get to where we wanted all along.
import { map } from 'fp-ts/Record';
import * as iots from 'io-ts';
export const partialRecord = <K extends string, T extends iots.Any>(
k: iots.KeyofType<Record<string, unknown>>,
type: T,
): iots.PartialC<Record<K, T>> => iots.partial(map(() => type)(k.keys));
const GeneralCodec = iots.unknown;
const SupportedEnvCodec = iots.keyof({
required:null,
optional:null,
});
type SupportedEnv = iots.TypeOf<typeof SupportedEnvCodec>;
type RequiredEnv = Extract<SupportedEnv, 'required'>;
const RequiredEnvCodec: iots.Type<RequiredEnv> = iots.literal('required');
type OtherEnvs = Exclude<SupportedEnv, RequiredEnv>;
const OtherEnvsCodec: iots.KeyofType<Record<OtherEnvs, unknown>> = iots.keyof({optional:null});
const OtherEnvsRecordCodec = partialRecord<
OtherEnvs,
typeof GeneralCodec
>(OtherEnvsCodec, GeneralCodec);
const SupportedCodec = iots.intersection([
iots.record(RequiredEnvCodec, GeneralCodec),
OtherEnvsRecordCodec,
]);
type Supported = iots.TypeOf<typeof SupportedCodec>;
This yields the desired type:
type Supported = {
required: unknown;
} & {
optional?: unknown;
}
Original Answer
The closest I've gotten is to add one extra level of indirection, which is unfortunate.
For example:
const GeneralCodec = iots.unknown;
const SupportedEnvCodec = iots.union([
iots.literal('required'),
iots.literal('optional'),
]);
type SupportedEnv = iots.TypeOf<typeof SupportedEnvCodec>;
type RequiredEnv = Extract<SupportedEnv, 'required'>;
const RequiredEnvCodec: iots.Type<RequiredEnv> = iots.literal('required');
type OtherEnvs = Exclude<SupportedEnv, RequiredEnv>;
const OtherEnvsCodec: iots.Type<OtherEnvs> = iots.union([
iots.literal('optional'),
]);
const SupportedCodec = iots.intersection([
iots.record(RequiredEnvCodec, GeneralCodec),
iots.partial({
env: iots.record(OtherEnvsCodec, GeneralCodec),
}),
]);
type Supported = iots.TypeOf<typeof SupportedCodec>;
This produces the type:
type Supported = {
required: unknown;
} & {
env?: {
optional: unknown;
} | undefined;
}
Which... is close - though I really wish I didn't need that extra level, but it does answer my original question in a roundabout way.