StephenWeiss
StephenWeiss

Reputation: 1073

io-ts: Using a literal union, define required and optional keys in an object

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

Answers (1)

StephenWeiss
StephenWeiss

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

Related Questions