Home > Back-end >  How can I make TypeScript error on missing mapped key?
How can I make TypeScript error on missing mapped key?

Time:03-02

This is what I have:

type TransferType = 'INTERNAL' | 'WITHDRAWAL' | 'DEPOSIT'

type TransferEvents = Record<TransferType, Record<string, TypeFoo | TypeBar>>

export interface EventsTooltip extends TransferEvents {
  some: string;
  extra: number;
  keys: boolean:
  INTERNAL: Record<string, TypeFoo>;
  WITHDRAWAL: Record<string, TypeBar>; 
  DEPOSIT: Record<string, TypeBar>;
}

Is it possible to make TypeScript throw an error if I add another type to TransferType, like 'CORRECTION', but forget to add it to EventsTooltip? While also retaining the ability to be more specific for the value type (TypeFoo or TypeBar) in the records?

CodePudding user response:

I can think of a couple of ways.

One is to define TransferType in terms of an object type that has those Record<string, TypeFoo> etc. values, like this:

interface TransferRecords { // Or whatever name makes sense
    INTERNAL: Record<string, TypeFoo>;
    WITHDRAWAL: Record<string, TypeBar>;
    DEPOSIT: Record<string, TypeBar>;
}

type TransferType = keyof TransferRecords; // Type is "INTERNAL" | "WITHDRAWAL" | "DEPOSIT"

export interface EventsTooltip extends TransferRecords {
    some: string;
    extra: number;
    keys: boolean;
};

Playground Link

With that, EventsTooltip and TransferType can't get out of sync because they both draw on the same source information (TransferRecords).

If you didn't want to do that, I can't come up with a way to make the type fail, but I can come up with a way to make using the type fail, like this:

type TransferType = "INTERNAL" | "WITHDRAWAL" | "DEPOSIT";

type BaseEventsTooltip = {
    some: string;
    extra: number;
    keys: boolean;
};

export type EventsTooltip = BaseEventsTooltip & {
    [key in TransferType]: key extends "INTERNAL"
        ? Record<string, TypeFoo>
        : key extends "WITHDRAWAL"
            ? Record<string, TypeBar>
            : key extends "DEPOSIT"
                ? Record<string, TypeBar>
                : never;
};

It's verbose and awkward, but it means that if you have some code like this somewhere;

const tooltip: EventsTooltip = {
    some: "x",
    extra: 42,
    keys: true,
    INTERNAL: /*...*/,
    WITHDRAWAL: /*...*/,
    DEPOSIT: /*...*/,
};

and later you add to TransferType, tooltip above will show an error because the new key won't be present (and can't be provided, because its type would be never). Hopefully that would reveal that you need to update the EventsTooltip type.

Playground with tooltip working

Playground with another element in TransferType causing tooltip to fail

It's clunky and I hope there's a better solution, but it works. :-)

CodePudding user response:

You can use a generic type that simply returns the generic after doing a compile-time check on the generic:

type TransferType = 'INTERNAL' | 'WITHDRAWAL' | 'DEPOSIT'

type TypeFoo = { __lock1: never };
type TypeBar = { __lock2: never };

type TransferEvents = Record<TransferType, Record<string, TypeFoo | TypeBar>>

type CreateEventsTooltip<
    // Compile-time check here.
    T extends TransferEvents,
> = T;

export type EventsTooltip = CreateEventsTooltip<{
  some: string;
  extra: number;
  keys: boolean;
  INTERNAL: Record<string, TypeFoo>;
  WITHDRAWAL: Record<string, TypeBar>; 
  DEPOSIT: Record<string, TypeBar>;
}>;

export type EventsTooltip2 = CreateEventsTooltip<{
  some: string;
  extra: number;
  keys: boolean;
  INTERNAL: Record<string, TypeFoo>;
  WITHDRAWAL: Record<string, TypeBar>; 
  // We're missing "DEPOSIT" so it errors
  // DEPOSIT: Record<string, TypeBar>;
}>;

TypeScript Playground Link

Edit: Also, if you want those index signatures with shallower typing back from you extending back when it was using interfaces, then you can change the CreateEventsTooltip type to look like this:

type CreateEventsTooltip<T extends TransferEvents> = T & TransferEvents;
  • Related