Search code examples
typescriptgenericsenumsconfigextends

Is assignable but could be instantiated with a different subtype


I want to create a generic function to work with the type of only passed argument and not any type from enum.

There is a simplified example.

enum EAnimals {
  dog = 'dog',
  cat = 'cat',
}

interface IConfig<T extends EAnimals> {
  animalType: T
}

const configs: {[animalType in EAnimals]: IConfig<animalType>} = {
  [EAnimals.cat]: {
    animalType: EAnimals.cat
  },
  [EAnimals.dog]: {
    animalType: EAnimals.dog
  }
}

function doSomething<T extends EAnimals> (animalTypeParam: T): IConfig<T> {
  return configs[animalTypeParam]; // error
}

I've seen 60730845 but I want to use generics. For example, here it's impossible to create config for EAnimals.cat where animalType is anything besides EAnimals.cat. If I do doSomething(EAnimals.cat) I will know that returned config is for cat. If I declare parameter's type as animalTypeParam: EAnimals I will have to also declare returned type as IConfig<EAnimals> which ruins the idea.

I understand that it happens because T extends ... can be bigger than original enum but I couldn't find something like in for generics.

The main purpose of this is to get rid of human factor when creating configs and functions to work with this configs. If there's another working solution, I would love to hear it.


Solution

  • Just overload it:

    enum EAnimals {
        dog = 'dog',
        cat = 'cat',
    }
    
    interface IConfig<T extends EAnimals> {
        animalType: T
    }
    
    const configs: { [animalType in EAnimals]: IConfig<animalType> } = {
        [EAnimals.cat]: {
            animalType: EAnimals.cat
        },
        [EAnimals.dog]: {
            animalType: EAnimals.dog
        }
    }
    
    function doSomething<T extends EAnimals>(animalTypeParam: T):IConfig<T>
    function doSomething<T extends EAnimals>(animalTypeParam: T) {
        return configs[animalTypeParam]; // ok
    }
    
    const result = doSomething(EAnimals.cat) // IConfig<EAnimals.cat>
    

    Playground

    UPDATE

    There are two alternative ways to handle it.

    First - make partial application

    enum EAnimals {
        dog = 'dog',
        cat = 'cat',
    }
    
    interface IConfig<T extends EAnimals> {
        animalType: T
    }
    
    const configs = {
        [EAnimals.cat]: {
            animalType: EAnimals.cat
        },
        [EAnimals.dog]: {
            animalType: EAnimals.dog
        }
    } as const
    
    type Config = { [animalType in EAnimals]: IConfig<animalType> }
    
    const doSomething = <C extends Config>(config: C) => <T extends EAnimals>(animalTypeParam: T) => {
        return config[animalTypeParam]; 
    }
    
    const result = doSomething(configs)(EAnimals.cat)
    

    But, then you need to make configs an immutable object.

    Second - don create config variable at all

    enum EAnimals {
        dog = 'dog',
        cat = 'cat',
    }
    
    interface IConfig<T extends EAnimals> {
        animalType: T
    }
    
    
    type Config = { [animalType in EAnimals]: IConfig<animalType> }
    
    const doSomething = <C extends Config>(config: C) => <T extends EAnimals>(animalTypeParam: T) => {
        return config[animalTypeParam];
    }
    
    const result = doSomething({
        [EAnimals.cat]: {
            animalType: EAnimals.cat
        },
        [EAnimals.dog]: {
            animalType: EAnimals.dog
        }
    })(EAnimals.cat)