typescript - io-ts: Använd en bokstavlig union för att definiera obligatoriska och valfria nycklar i ett objekt


Jag försöker definiera en ny codec med io-ts.

Formen ska se ut så här när jag är klar:

type General = unknown;
type SupportedEnv = 'required' | 'optional'

type Supported = {
  required: General;
  optional?: General;
}

(Observera: för tillfället är formen på General inte viktig)

Nyckeln här är att jag vill härleda typen baserat på General och SupportedEnv.

För närvarande har jag något som liknar:

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>; 

Typen Supported har båda nycklarna som krävs:

type Supported = {
  required: General;
  optional: General;
}

Hur kan jag göra så att optional verkligen är valfritt?

Jag har försökt använda en intersection och partial... men jag kan inte klura ut syntaxen med iots.record.

Är detta möjligt? Behöver jag tänka på detta på ett annat sätt?

Svar #1

Uppdatering

Genom att skapa en avbildningsfunktion partialRecord (som utnyttjar io-ts's peer-dep på fp-ts), kan vi nå dit vi alltid ville.

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>;

Detta ger den önskade typen:

type Supported = {
    required: unknown;
} & {
    optional?: unknown;
}

Originalt svar

Det närmaste jag har kommit är att lägga till ett extra nivå av indirektion, vilket är olyckligt.

Till exempel:

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>; 

Detta producerar typen:

type Supported = {
    required: unknown;
  } & {
    env?: {
        optional: unknown;
    } | undefined;
}

Vilket... är nära - även om jag verkligen önskar att jag inte behövde den där extra nivån, men det besvarar ändå min ursprungliga fråga på ett omvänt sätt.