0

First, I'll just provide some background info:

I have the two interfaces that both inherit off of a common base.

interface Base { type: string }
interface Foo extends Base { type: "foo", name: string }
interface Bar extends Base { type: "bar", score: number }

I keep both in a union type so I can use it in an array that stores either of the two.

type Baz = Foo | Bar;
const storage : Baz[] = [];

I have a type which stores all possible values of type for the union type, and a function that only accepts those values.

type BazTypes = Baz["type"]; // "foo" | "bar"
function readBaz(type : BazTypes) { /* ... */ }

Now, I'm trying to make a map of functions that will parse a given object as presented by Baz, given its type, like so:

// Expectation:
{
    "foo": (arg : Foo) => boolean, // (arg: {type: "foo", name: string}) => boolean
    "bar": (arg : Bar) => boolean, // (arg: {type: "bar", score: number}) => boolean
}

type BazMap = {
    [key in BazTypes]: (args : Baz extends {type: key} ? Baz : never) => boolean
};
const parsers : BazMap = {
    "foo": (foo /* which should be of type Foo */) => { /* ... foo.name ... */ },
    "bar": (bar /* which should be of type Bar */) => { /* ... bar.score ... */ },
}

However, that map instead provides the union type itself instead of the correct interface for the given type.

// Reality:
{
    "foo": (arg : Foo | Bar) => boolean,
    "bar": (arg : Foo | Bar) => boolean
}

type BazMap = {
    [key in BazTypes]: (args : Baz extends {type: key} ? Baz : never) => boolean
};
const parsers : BazMap = {
    "foo": (foo /* which is Foo | Bar */) => { /* Only foo.type can be accessed. */ },
    "bar": (bar /* which is Foo | Bar */) => { /* Only bar.type can be accessed. */ },
}

Because of this, TypeScript does not provide me either name and score since either value does not exist in both Foo nor Bar.

I am trying to get the map as I expected. So far, I haven't been able to properly create a map that accepts the correct type.

TL;DR: How can I create a Object type based on the parameter of a type? (e.g. If the type is "foo", it will return a parser for Foo (that doesn't accept Bar), etc.)

Chlod Alejandro
  • 566
  • 8
  • 16

2 Answers2

2

You can use the UnionToIntersection from this answer:

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type Parsers<T extends Base> = UnionToIntersection<
  // Distributes T
  T extends any
    // {foo: (args: Foo) => boolean} but with each T
    ? {[_ in T['type']]: (args: T) => boolean}
    : never
>;

// {foo: (args: Foo) => boolean} & {bar: (args: Bar) => boolean}
type BazMap = Parsers<Baz>;

Playground link

Lauren Yim
  • 12,700
  • 2
  • 32
  • 59
1

@cherryblossom's answer is really slick, but here's a simpler way:

type BazMap = {
    [key in BazTypes]: (args: Baz & {type: key}) => boolean
};

I'm just changing the one line in your setup that's causing the error.

Your version doesn't work because your conditional Baz extends {type: key} ? Baz : never will always return never. Baz is not a generic. You are asking it about your predefined union type Baz, and as a union type it does not extend the {type: __} properties of its members.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102