Reputation: 1073
I'm trying to define a new codec with io-ts
.
The shape should look like the following when I'm done:
type General = unknown;
type SupportedEnv = 'required' | 'optional'
type Supported = {
required: General;
optional?: General;
}
(Note: for the time being the shape of General
is not important)
The key here is that I want to derive the type based on General
and SupportedEnv
.
Currently, I have something like:
const GeneralCodec = iots.unknown;
const SupportedEnvCodec = iots.union([
iots.literal('required'),
iots.literal('optional'),
]);
const SupportedCodec = iots.record(SupportedEnvCodec, GeneralCodec)
type Supported = iots.TypeOf<typeof SupportedCodec>;
The type Supported
has both keys required:
type Supported = {
required: General;
optional: General;
}
How can I make it so that optional
is indeed optional?
I've tried using an intersection and partial... but I can't figure out the syntax with iots.record
.
Is this possible? Do I need to think about this differently?
Upvotes: 2
Views: 1220
Reputation: 1073
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.
Upvotes: 0