#Typing a generic function using ThisType of object it is called in?

24 messages · Page 1 of 1 (latest)

final blaze
#

Hello, I would like some help about a "complex" TypeScript issue I have. I wan't to create a generic function which memoize a callback value (a getter) to be using the ThisType of the object it is used in. Here is a code example which makes that clearer. I don't even know if this is possible.

declare const makeDefinition: <O extends {}>(obj: O & ThisType<O>) => O;

// If want `unknown` to be `ThisType` of the calling object.
declare const memoized: <T>(compute: (obj: unknown) => T) => T;

const definition = makeDefinition({
  firstName: 'John',
  lastName: 'Doe',
  // So `obj` in memoized is correctly typed to the current `ThisType`.
  fullName1: memoized((obj) => `${obj.firstName} ${obj.lastName}`),
  // Just like in the following getter.
  get fullName2() {
    return `${this.firstName} ${this.lastName}`;
  },
});
strong cosmos
#

First of all, your definition of makeDefinition is correct, as you’re using ThisType<O> to ensure that the this within the callback function will correctly refer to the type of the object. This allows methods like fullName1 and fullName2 to be properly typed with respect to their containing object.

#

And I think to fix the issue, the memoized function must be written to accept a function whose this is of type 0.

#

declare const makeDefinition: <O extends {}>(obj: O & ThisType<O>) => O;

declare const memoized: <T, O>(compute: (this: O) => T) => T;

const definition = makeDefinition({
firstName: 'John',
lastName: 'Doe',

fullName1: memoized(function (this: typeof definition) {
return ${this.firstName} ${this.lastName};
}),

get fullName2() {
return ${this.firstName} ${this.lastName};
},
});

final blaze
#

Thanks @strong cosmos for your answer, that's something I already tried. What I want is to be able to infer the obj type inside an arrow function. That will make the use of memoized quick and easy, without having to manually strongly type the definition. In addition, this will also avoid potential TS2310: Type User recursively references itself as a base type.

final blaze
#

!helper

solar jungle
balmy nicheBOT
#
balam314#0

Preview:ts ... fullName1: function () { return `${this.firstName} ${this.lastName}` ...

solar jungle
#

putting parentheses around that function expression already breaks it

final blaze
#

Yes, and it breaks the use of arrow function, that's why I wish to use a param typing instead of a this typing.

solar jungle
#

@final blaze would this work for your use case?

balmy nicheBOT
#
balam314#0

Preview:```ts
function makeDefinition<T1 extends {}, T2>(
obj: T1,
part2: (obj: T1) => T2
): T1 & T2 {
return Object.assign(obj, part2(obj))
}

// If want unknown to be ThisType of the calling object.
declare const memoized: <T>(
compute: (obj: unknown) => T
) => T

const definition = makeDefinition(
{
firstName: "John"
...```

solar jungle
#

the type is still inferred correctly after wrapping it in memoized()

final blaze
#

Thanks for this solution. That might be a working one, but it is not ideal as it will require a second parameter. In addition, my goal is to use a proxy to correctly implement memoize, which is not compatible with that single callback way (if there are multiple memoized props in the second definition).

#

Further more, it won't work if a memoized prop requires using another memoized prop.

#

PS: for the context, this is a very simple example, but my goal is to integrate that inside my lib's models (foscia.dev)

#

I don't even know if this is possible, I might create an issue on typescript repo if nobody found a solution here.

nocturne lava
#

@final blaze mostly out of curiosity, do you have implementations for memoize and makeDefinition?

final blaze
#

Yes, but that's a bit complicated because it's integrated in a lib that's way bigger.

#

I'll send the code for memoized soon

#

@nocturne lava and here is the "assembled" function (memoize in my lib). This is far more complex than the simple playground example I sent, but the goal is pretty the same:

const assembled = <T>(
  config: ((instance: any) => T) | (ModelAssembledFactoryConfig<T> & {
    get?: (instance: any) => T;
    set?: (instance: any, value: T) => void;
  }),
  otherConfig?: ModelAssembledFactoryConfig<T>,
) => {
  const { get, set, ...props } = (
    typeof config === 'function' ? { get: config, ...otherConfig } : config
  );

  return makePropChainableFactory({
    readOnly: !set,
    sync: false,
    memo: true,
    ...props,
    init(instance) {
      const noMemoSymbol = Symbol('');
      let memoValue = noMemoSymbol as unknown;

      const previousValues = new Map<PropertyKey, unknown>();

      const shouldCompute = () => (
        !this.memo || memoValue === noMemoSymbol || [...previousValues.entries()].some(
          ([key, value]) => !instance.$model.$config
            .compareSnapshotValues(value, Reflect.get(instance, key)),
        )
      );

      const compute = () => {
        previousValues.clear();

        memoValue = get!(new Proxy(instance, {
          get: (...params) => tap(
            Reflect.get(...params),
            (value) => previousValues.set(params[1], value),
          ),
        }));

        return memoValue;
      };

      Object.defineProperty(instance, this.key, {
        enumerable: true,
        get: get ? () => (shouldCompute() ? compute() : memoValue) : undefined,
        set: set ? (value) => set(instance, value) : undefined,
      });
    },
  }, {
    alias: (alias: string) => ({ alias }),
    sync: (sync?: boolean | ModelPropSync) => ({ sync: sync ?? true }),
    memo: (memo?: boolean) => ({ memo: memo ?? true }),
  }) as ModelAssembledFactory<T, boolean>;
};

export default assembled;
final blaze
#

I'll try a last time 😅