TypeScript: Infer type of object of generic interface implicitly from object content

Viewed 723

I have a straightforward case in TypeScript, where I have an interface with a generic type T that extends an enum E. Based on the type a config type is assigned.

enum E { "A", "B" }

type ConfigsBase = { [K in E]: object }

interface Configs extends ConfigsBase {
    [E.A]: { a: string };
    [E.B]: { b: number };
}

interface MyInterface<T extends E> {
  type: T;
  config: Configs[T];
}

The question is how I can use this interface in a manner that the generic type is implicitly inferred from the object. So that I get useful type errors when the config is wrong.

const a: ??? = {
  type: E.A,
  config: { a: 1 }, 
}
// => I want this to give a typescript error because 1 is not a string

What do I need to insert for ??? so that typescript gives an error that the config is wrong. It should be something like const a: MyInterface<look for yourself what T is>.

I found a relatively tedious solution by using an identity function which infers the type automatically:

const inferType = <T extends E>(obj: MyInterface<T>): MyInterface<T> => obj

const a = inferType({
  type: E.A,
  config: { a: 1 },
})

// => TypeScript error: Type 'number' is not assignable to type 'string' 

Is this possible to do this more elegantly with a type annotation without using a function?

3 Answers

there is a way with infer operator. See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-inference-in-conditional-types.

You can do this:

enum E { "A", "B" }

type ConfigsBase = { [K in E]: object }

interface Configs extends ConfigsBase {
    [E.A]: { a: number };
    [E.B]: { b: number };
}

interface MyInterface<T extends E> {
  type: T;
  config: Configs[T];
}

type InferType<obj> = obj extends MyInterface<infer R> ? MyInterface<R> : never;

const variable = {
    type: E.A,
    config: { a: 1 }
};

const inferredVariable: InferType<typeof variable> = variable;

It will be faster because you don't use functions.

Updated Playground

Using the sealed class concept is probably the cleanest and most explicit approach.
It also does not require any intermediate types or functions:

enum E { "A", "B" }

interface MyTypeBase<T extends E> {
  type: T;
}

interface MyTypeA extends MyTypeBase<E.A> {
  config: { a: string };
}

interface MyTypeB extends MyTypeBase<E.B> {
  config: { b: number };
}

type MyType = MyTypeA | MyTypeB;

const a: MyType = {
  type: E.A,
  config: { a: 1 }, // <-- will give type error as expected
};

In TypeScript Playground

In kotlin you would use sealed classes:

sealed class Config {
    data class ConfigA(val a: String): Config()
    data class ConfigB(val b: Int): Config()
}

val a = Config.ConfigA(1)//type error here

fun main() {
    val c : Config = a

    when (c) {// no type property needed
        is Config.ConfigA -> c.a
        is Config.ConfigB -> c.b
    }
}
Related