Typescript: Base a type on the corresponding Type in a Map

Viewed 63

I'm using Firestore and I want to associate the paths in my database with the right interface.

Let's say I have 3 possible paths. Here's the type of those paths:

type FsPath =
  `badges/${string}` |
  'entities' |
  `entities/${string}`;

In the path entities and entities/${string} I only want to store data with type ServerEntity and in the path badges/${string} I only want to store data with type ServerBadge;

interface ServerEntity {
  name: string;
  age: number;
}

interface ServerBadge {
  level: number;
  label: string;
}

Now let's say I have this function

setValue(path: FsPath, value: any) {
  // I want to know how to replace the any with a deduced type from the FsPath of "path"
}

What should I write instead of "any" to make sure I can't do the following

const badge: ServerBadge = {
  level: 4,
  label: 'Best badge'
};

setValue('entities/qs89sdjdziU', badge); // I want this to raise an TS error

How can I create a "Map" (not sure of the term) between my FsPath and my interfaces ServerBadge, ServerEntity? Then how can I check on this "Map" to type the "value" key in setValue()?

Hope the question is clear enough.

Thanks

EDIT: added ServerBadge and ServerEntity types

EDIT after @captain-yossarian answer:

Is there a way to create a PathMap but with the whole path as keys instead ?

type PathMap = {
  'badges': ServerBadge[],
  `badges/${string}`: ServerBadge,
  'entities': ServerEntity[],
  `entities/${string}`: ServerEntity
}

I know this won't work because a type key can't be a TS type. So how could I create a Map between 2 types ?

EDIT Added a modified playground with the subcollection issue:

Playground

1 Answers

Consider this example:

interface ServerEntity {
    name: string;
    age: number;
}

interface ServerPlace {
    title: string;
    city: string;
}

interface ServerBadge {
    level: number;
    label: string;
}

type FsPath =
    | 'badges'
    | `badges/${string}`
    | 'entities'
    | `entities/${string}`
    | `entities/${string}/places`
    | `entities/${string}/places/${string}`;


type FsMap = {
    'entities': ServerEntity,
    'badges': ServerBadge
}

type Validate<T> = T extends FsPath ? T : never

type GetPrefix<T extends string> =
    (T extends `${infer Fst}/${infer _}`
        ? Validate<Fst>
        : (T extends `${infer Fst}`
            ? Validate<Fst> : never
        )
    );

type GetValue<T extends FsPath> = GetPrefix<T> extends keyof FsMap ? FsMap[GetPrefix<T>] : never

function setValue<P extends FsPath>(path: P, value: GetValue<P>) { }

const badge: ServerBadge = {
    level: 4,
    label: 'Best badge'
};

const entity: ServerEntity = {
    name: 'hello',
    age: 42
}

setValue('entities/qs89sdjdziU', entity); // ok
setValue('badges', badge); // ok

setValue('entities/qs89sdjdziU', badge); // error

Playground

PathMap is just a dictionary, nothing special.

GetValue splits the path into two parts ${first part}/${second part}. Then, it checks whether first part is a key of dictionary or not. If it is - get value by key, otherwise - return never.

You can find more interesting examples regarding function arguments validation in my article

Related