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. :-)