#Avoid Widening Classes in a Record when they have a Default Generic Parameter

23 messages · Page 1 of 1 (latest)

tired loom
#
declare class GenericClass<T, U = T> {
  constructor(value: T);

  u: U;
}

function takesRecordOfGenericClass<
  R extends Record<string, GenericClass<any, any>>
>(r: R): R {
  return r;
}

const genericClass = new GenericClass(1);
                  // ^ NumberField<number, number>

const record = {
  property1: new GenericClass(1)
          // ^ NumberField<number, number>
}

const widenedInference = takesRecordOfGenericClass({
  property1: new GenericClass(1)
          // ^ NumberField<number, any>
          // How do I get this to infer as `NumberField<number, number>` like it normally does?
});

Here's a playground link

I feel the code speaks more clearly than I can. It seems that when inferring for R extends Record<string, GenericClass<any, any>> it ends up widening the inner properties. This does NOT happen with just a single field, e.g. in a similar function with T extends GenericClass<any, any> the inference is always exact. For some reason though Record<string, GenericClass<any, any>> ends up widening each value.

This does NOT happen with R extends any, R extends object, R extends Record<string, unknown> or anything else I can throw at it. It still happens during subclassing.

uncut zephyrBOT
#

@tired loom Here's a shortened URL of your playground link! You can remove the full link from your message.

lukeabby#0

Preview:```ts
declare class GenericClass<T, U = T> {
constructor(value: T);

u: U;
}

function takesRecordOfGenericClass<
R extends Record<string, any>

(r: R): R {
return r;
}

const genericClass = new GenericClass(1);
// ^ NumberField<number, number>
...```

drowsy agate
#

I don't see the any issue in playground.

tired loom
#

Avoid Widening Classes in a Record when they have a Default Generic Parameter

#

Ah sorry

#

I posted the version with Record<string, any>

#

Should've been Record<string, GenericClass<any, any>> in the playground to reproduce the problem

#

edited

#

I've also tried declare class GenericClass<const T, const U = T> { because I wondered if it was some legacy inference decision that was updated with const type parameters but that didn't change much. Now it's infered as GenericClass<1, any> which still isn't what I wanted.

drowsy agate
#

Honestly not sure why it does that, but a simple fix is to noop map over R:

uncut zephyrBOT
#
declare class GenericClass<T, U = T> {
    constructor(value: T)
    u: U
}

function takesRecordOfGenericClass<
    R extends Record<string, GenericClass<any, any>>,
>(r: { [K in keyof R]: R[K] }): R {
    return r
}

const widenedInference = takesRecordOfGenericClass({
    property1: new GenericClass(1),
    property2: new GenericClass('foo'),
})

widenedInference
// ^? - const widenedInference: {
//     property1: GenericClass<number, number>;
//     property2: GenericClass<string, string>;
// }
drowsy agate
#

What's the purpose of U in GenericClass, how is it intended to be used?

tired loom
#

Well the no-op map solution is good enough for me. Good old homomorphic mappings!

tired loom
#

They have a "runtime type" T that can be anything convenient like a Set or so on

#

And then they have a serializable type that's technically arbitrary but in reality some simplifications of those types that fits in the db.

#

I personally would've modeled it differently but the types also depend on the constructor etc. hence why it's still generic

#

It's not like you just say "All sets serialize as an array and all numbers serialize as... well numbers" because depending on the options the serialization strategy might change

#

Again would've modeled it differently but it led to two generic parameters. One for the runtime type and one for the serialized type (which is derived from the runtime type and the options)

drowsy agate
#

I'm mostly asking because if you instead have something like:

declare class GenericClass<T, U> {
    constructor(value: T, option: U)
    u: DoSomethingWith<T, U>
}

Then you wouldn't need the work around, your original code should just work with no issue.

#

Alternatively if you have something like:

declare class GenericClass<T, U> {
    constructor(value: T)
    u: U
}

declare class Foo extends GenericClass<number, number> {}

declare class Bar extends GenericClass<boolean, string> {}

const widenedInference = takesRecordOfGenericClass({
    property1: new Foo(1),
    property2: new Bar(true),
})

It will also work without any work around.

tired loom
#

Yeah I might try to restructure how it works but the serialization strategy doesn't just depend on the constuctor but some abstract overrides

#

Thanks for finding that backwards inference of homomorphic mapped types solve it here though.