Question: Type inference on function parameters with nested generics?

I am facing an issue with Typescript generics.
I have a function which must be called with a type and some attributes depending on this type.
Typescript does not manage to infer the type of attributes when inside an if guarding the type to TYPES.ME:
enum TYPES {
    ME = 'me',
    YOU = 'you'
}

type Attributes<T extends TYPES> =  T extends TYPES.ME ? {keys: true} : {hat: true}
const func = <T extends TYPES>(type: T, attr: Attributes<T>) => {
    if(type === TYPES.ME) {
        attr.keys // error Property 'keys' does not exist on type '{ keys: true; } | { hat: true; }'
    }
} 

Playground link
I would like to have your opinions on this matter and see if there is a nice workaround.
Cheers!


Answer:
Currently, TypeScript is unable to re-constrain generic type parameters as a result of control flow analysis.  Inside the body of func(type, attr), you check that type === Types.ME.  This can narrow the type of type from T to something like T & Types.ME.  But it cannot do anything to T itself.  The type parameter T stubbornly stays the same; it is not constrained to Types.ME. And thus the compiler cannot conclude that attr is of type Attributes<Types.ME>.
And it is technically correct for the compiler to refuse to change T.  That's because, while individual values like type can only be one thing at a time, a type argument like T can be a union.  Indeed, you can call func() with a T equal to the full Types.ME | Types.YOU union, like so:
func(
    Math.random() < 0.999 ? Types.ME : Types.YOU,
    { hat: true }
) // compiles without error

If you inspect that, you'll see that T is inferred as Types (the full union of Types.ME | Types.YOU, and therefore attr is allowed to be {hat: true} even in the 99.9% likely event that type is Types.ME.
There is a longstanding open feature request at microsoft/TypeScript#27808 which asks for a way to say "T will be exactly one of Types.ME or Types.YOU; it cannot be a union".  And then, maybe inside the function body, checking type === Types.ME would allow T itself to be constrained to Types.ME, and things would work as expected.  And presumably the call with Math.random() < 0.999 would be rejected.
But for now it's not part of the language.

You might consider taking the approach where instead of having func be generic, you make it similar to an overloaded function, where it has one call signature per member of Types. You can write that as a function with a rest parameter whose type is a discriminated union of tuple types, and the compiler will treat it as such inside the function body.
Perhaps like this:
type FuncArg =
    [type: Types.ME, attr: { keys: true }] |
    [type: Types.YOU, attr: { hat: true }];

const func: (...args: FuncArg) => void = (type, attr) => {
    if (type === Types.ME) {
        console.log(attr.keys) // this works
    }
}

And now you can't make the invalid call:
func(
    Math.random() < 0.999 ? Types.ME : Types.YOU,
    { hat: true }
) // error, type 'Types.ME' is not assignable to type 'Types.YOU'

Everything works because now type and attr are bound together as desired in FuncArg; it's like imagining T were constrained to be just one member of Types at a time, and walking through the possibilities.  Note that FuncArg could, if necessary, be computed from the Attributes type given in the question, or from another mapping interface, but that is out of scope for the question as asked.
Playground link to code