1

I have a bunch of model classes that each have a model type. I also have a "TypeToModel" helper to get the model class given a model type. I.e.:

type BaseModel = { id: number };

type Models = {
    user: BaseModel & { name: string },
    post: BaseModel & { title: string },
};

type ModelType = 'user' | 'post';

type TypeToModel<T extends keyof Models> = ModelType extends T
    ? BaseModel
    : Models[T];

function getModel<T extends ModelType>(
    modelType: T,
): TypeToModel<T> {
    return {} as unknown as TypeToModel<T>;
}

If a single model type is passed to getModel, I want the return type to be the model's type. If the entire ModelType union gets passed to getModel, I want it to return BaseModel. This works most of the time:

const userA = getModel('user'); // userA = BaseModel & { name: string }
const userB = getModel('user' as ModelType); // userB = BaseModel

However, if a variable with a generic type gets passed to getModel, it returns a union of all the models. E.g.:

function foo<T extends ModelType>(modelType: T) {
    const userC = getModel(modelType);
    userC.id;
    /*
    userC = BaseModel
        | (BaseModel & { name: string })
        | (BaseModel & { title: string })
    */
}

I expected userC to be BaseModel, but it's a union of all the models. I want to detect if it would return a union of all the models and make it return BaseModel instead. I tried using IsUnion<>, but it was false for both T and userC.

How can I check if getModel's argument has a generic type? In other words, is it possible to write an IsGeneric<> utility type?

TS Playground

Leo Jiang
  • 24,497
  • 49
  • 154
  • 284
  • "it slows down `tsc` significantly" What's going on in that code base where a union slows down the compiler to such a grave extent? I'd be more worried about *that* than the problem you're describing here. It's like you said "Do they make toenail clippers specifically for left-handed people? I'm having trouble using 'regular' clippers with my left hand; it's causing problems because my toenails have started growing about ten centimeters per day, and I'd use my right hand but it recently fell off. Any recommendations?" – jcalz Nov 01 '22 at 16:20
  • 1
    As for the question as asked, there seem to be multiple. "What's going on here?" When you have a value whose type depends on a generic type parameter, there is generally a tradeoff between *deferred* evaluation (where the type is always "correct" but often useless because it is essentially opaque to the compiler), and *premature* evaluation (where it is a specific type you can use, but often incorrect because the generics have been summarily substituted with their constraints). The compiler uses heuristics to determine when to do which. Property access tends to evaluate early. – jcalz Nov 01 '22 at 16:27
  • "How can I check if X is an indeterminate generic variable?" That's either impossible or possible-but-fragile, since the same mechanisms that defer evaluation of complex types involving generics would probably defer the evaluation of whatever check we invent. And if they didn't, I'd be wary of trusting the result. This is an interesting question in terms of playing with the language, but I can't imagine that you'd want to actually use anything that comes out of it. – jcalz Nov 01 '22 at 16:31
  • All this is saying I think you should probably [edit] the question to ask a single, well-defined question that isn't an XY problem. Right now it feels like your real problem is some performance issue, which you've been trying to fix with generic conditional types. If you want to solve that underlying problem, then the question could be refactored to ask about that. If you don't want to do that, maybe remove the parts that allude to performance issues, and clarify which one of your two questions is primary, preferably by removing the other one. – jcalz Nov 01 '22 at 16:35
  • If you do this and want me to take another look and possibly provide an answer, please comment and mention @jcalz so I'm notified. Good luck! – jcalz Nov 01 '22 at 16:35
  • Although I agree this setup is convoluted and we are likely in an XY problem situation, I find the original question too valuable to be edited away. I would rather ask a distinct question regarding the performance issue. Where else to ask this kind of question if not on a knowledge base? – geoffrey Nov 01 '22 at 17:11
  • @geoffrey right, so if we really want to know the answer to one of the questions, we should maybe edit away the performance part so it isn't lurking underneath. For example, maybe it should just be "how can I detect if a type is generic? Is there any way to write `type IsGeneric = Magic ? true : false` so that `function foo(){ type X = IsGeneric, type Y = IsGeneric }` has `X` evaluate to `false` and `Y` evaluate to `true`? " without the other parts – jcalz Nov 01 '22 at 17:22
  • 1
    I regret that questions requesting explanations don't get as much attention as questions requesting solutions (I had the same experience recently) but I don't want to speak for Leo. – geoffrey Nov 01 '22 at 18:00
  • I provided some unnecessary context (the performance part) because if I asked without the context, I'd expect people to comment "you don't need to return BaseModel, just return a union". Here's another question for the performance part: https://stackoverflow.com/q/70459685/599184 – Leo Jiang Nov 02 '22 at 00:34
  • 1
    @jcalz I removed the performance part from the question, now it's only about IsGeneric<>. If I remove the getModel part too, inevitably people will ask "why do you need this". Thanks for your hint about property access evaluating early, I might be able to write a hack for my specific performance issue – Leo Jiang Nov 02 '22 at 00:49
  • Figured out a hack that worked, here's a simplified question on finding a proper solution rather than the hack: https://stackoverflow.com/q/74283663/599184 – Leo Jiang Nov 02 '22 at 02:10
  • Thanks for the link to the performance question, that helps. If the question here is now "is it possible to write an `IsGeneric` utility type?" could you [edit] to explicitly describe what the type does (such as my `foo()` example)? I'd be happy to do it if you don't want to. (I still expect the answer here to be "no, this is impossible", but it would be nice to have some authoritative source on that, so after some unsuccessful tries to implement it, I'll probably start hunting through github issues to see if anyone else has asked this before) – jcalz Nov 02 '22 at 02:49

1 Answers1

1

To my knowledge there is no way to detect that state procedurally from within your utility type and react to it. I have no clue what this is called by the way.

You can also witness control flow analysis giving up:

function foo<T extends ModelType>(modelType: T) {
    if(modelType == 'user') {
        modelType; // modelType: T extends ModelType
    }
}

T is not ModelType. As you say, it's undetermined. so ModelType extends T cannot be resolved and the output of TypeToModel is the whole conditional ModelType extends T ? BaseModel : Models[T].

I would not try to understand why indexing id changes anything because I'm pretty sure TS goes through different code paths to handle this and it just so happens that their behaviour is inconsistent: TypeToModel<T>['id'] would not be resolved to a union, it would just remain as it is.

geoffrey
  • 2,080
  • 9
  • 13