How to represent the mapping between two data stores as a TypeScript union type

Viewed 45

I am very much a novice in TypeScript and I spent at least half my time just randomly changing variables to if the IntelliSense error clears.

I am working on a system that pulls in Ceramic streams and caches them in a Hasura instance. The fields in Ceramic sometimes have slightly different names than what is used in Hasura. The system imports profile information from two streams: Ceramic’s basicProfile & a homegrown extendedProfile.

I am trying to use union types to capture the field name mappings, but things aren't quite working. My types are unions where the key is the Hasura id and the value is the Ceramic id:

export const BasicProfileImages = {
  imageURL: 'image',
  bannerImageURL: 'background',
} as const;
export const BasicProfileStrings = {
  name: 'name',
  description: 'description',
  emoji: 'emoji',
  location: 'homeLocation',
  countryCode: 'residenceCountry',
  birthDate: 'birthDate',
  website: 'url',
} as const;
export const BasicProfileFields = {
  ...BasicProfileImages,
  ...BasicProfileStrings,
} as const;

export const ExtendedProfileImages = {
  backgroundImageURL: 'background',
} as const;
export const ExtendedProfileStrings = {
  username: 'username',
  pronouns: 'pronouns',
  magicDisposition: 'magicDisposition',
} as const;

export type BPImages = {
  -readonly [key in keyof typeof BasicProfileImages as string]?: ImageSources;
};
export type BPStrings = {
  -readonly [key in keyof typeof BasicProfileStrings as string]?: string;
};

export type EPImages = {
  -readonly [key in keyof typeof ExtendedProfileImages as string]?: ImageSources;
};
export type EPStrings = {
  -readonly [key in keyof typeof ExtendedProfileStrings as string]?: string;
};

export interface TimeZone {
  name?: string;
  abbreviation: string;
  location?: string;
  offset: number;
}
export interface TitledDescription {
  title: string;
  description?: string;
}
export type EPObjects = {
  availableHours?: number;
  timeZone?: TimeZone;
  playerType?: TitledDescription;
}

export type ProfileProps = (
  BPStrings | BPImages | EPImages | EPStrings | EPObjects
);

export const Images = {
  ...BasicProfileImages,
  ...ExtendedProfileImages,
} as const;
export const Strings = {
  ...BasicProfileStrings,
  ...ExtendedProfileStrings,
} as const;

export type Endpoints = {
  -readonly [key in keyof typeof Images]?: {
    val: string;
    set: () => void;
    ref: MutableRefObject<HTMLImageElement>;
  };
};

Then in my code I iterate over the Images and generate a URL value, setter and a reference for each image:

  const endpoints = Object.fromEntries(
    Object.keys(Images).map((hasuraId) => {
      const key = hasuraId as keyof typeof Images;
      // eslint-disable-next-line react-hooks/rules-of-hooks
      const [url, set] = useState<Maybe<string>>(
        player?.profile?.[key] ?? null,
      );
      // eslint-disable-next-line react-hooks/rules-of-hooks
      const ref = useRef<HTMLImageElement>(null);
      return [key, { val: url, set, ref }]; // key ends in “URL”
    }),
  );

Then I iterate over that structure in my onSubmit handler to access the selected images:

    const onSubmit = async (inputs: ProfileProps) => {
      ⋮
      const files: Record<string, File> = {};
      Object.keys(Images).forEach((hasuraId) => {
        const key = hasuraId as keyof typeof Images;
        const fileList = (inputs[key] ?? []) as File[];
        if (fileList.length > 0) {
          [files[key]] = fileList;
        }
        delete inputs[key]; // eslint-disable-line no-param-reassign
      });

The error I'm getting is for inputs[key]:

const key: "backgroundImageURL" | "imageURL" | "bannerImageURL"
Element implicitly has an 'any' type because expression of type '"imageURL" | "bannerImageURL" | "backgroundImageURL"' can't be used to index type 'ProfileProps'.
  Property 'imageURL' does not exist on type 'ProfileProps'.
1 Answers

I finagled the types a bit and ended up with:

export const BasicProfileImages = {
  imageURL: 'image',
  bannerImageURL: 'background',
} as const;
export const BasicProfileStrings = {
  name: 'name',
  description: 'description',
  emoji: 'emoji',
  location: 'homeLocation',
  countryCode: 'residenceCountry',
  birthDate: 'birthDate',
  website: 'url',
} as const;
export const BasicProfileFields = {
  ...BasicProfileImages,
  ...BasicProfileStrings,
} as const;

export const ExtendedProfileImages = {
  backgroundImageURL: 'background',
} as const;
export const ExtendedProfileStrings = {
  username: 'username',
  pronouns: 'pronouns',
  magicDisposition: 'magicDisposition',
} as const;

export interface TimeZone {
  name?: string;
  abbreviation: string;
  location?: string;
  offset: number;
}
export interface TitledDescription {
  title: string;
  description?: string;
}
export type EPObjects = {
  availableHours?: number;
  timeZone?: TimeZone;
  playerType?: TitledDescription;
}

export type BPImages = {
  -readonly [key in keyof typeof BasicProfileImages]?: ImageSources;
};
export type BPStrings = {
  -readonly [key in keyof typeof BasicProfileStrings]?: string;
};

export type EPImages = {
  -readonly [key in keyof typeof ExtendedProfileImages]?: ImageSources;
};
export type EPStrings = {
  -readonly [key in keyof typeof ExtendedProfileStrings]?: string;
};

export type ProfileProps = (
  BPImages & BPStrings & EPImages & EPStrings & EPObjects
);

The biggest help in getting it working was switching the |s for &s in ProfileProps. That and I removed the cast to string for key in keyof typeof BasicProfileImages, and the system gained a much clearer understanding of my types. *(The IntelliSense hover showed specific key / value pairs instead of simply [x: string]: string.

Related