hybridsjs / hybrids

Extraordinary JavaScript UI framework with unique declarative and functional architecture
https://hybrids.js.org
MIT License
3.03k stars 85 forks source link

Small Type Inferencing Issue with Property<E, V> #249

Closed au-z closed 2 months ago

au-z commented 5 months ago

Hi @smalluban!

I was working on some side project and noticed that the type inferencing when passing properties as arguments has a small problem.

When passing a computed getter function to a factory function, the property is inferred as a function:

function factory<E, V>(property: Property<E, V>) {
  return property;
}

define<CustomElement>({
  tag: 'custom-element',
  foo: '',
  // inferred incorrectly as Property<CustomElement, (host: CustomElement) => string>
  factoryProperty: factory((host: CustomElement) => host.foo),
})

I was able to reproduce the issue here and I show a possible fix: https://stackblitz.com/edit/vitejs-vite-xmqndb?file=package.json,src%2Fmain.ts,tsconfig.json&terminal=dev

In my experimenting, I found out that rearranging the union type for Property made a difference:

type Property<E, V> =
  | ((host: E & HTMLElement, lastValue: V) => V)
  | (V extends string | number | boolean | undefined ? V : never)
  | Descriptor<E, V>
  | undefined;

If you think re-arranging the union type wouldn't create any unforseen negative side-effects, I'm happy to author a quick PR. 👍

Qsppl commented 5 months ago

First error:

You broke the Component<CustomElement> type by mapping in your interface:

image

Let's fix this:

image

Now we have arrived at the same problem the right way

The factory(() => string) function compares two types: Property<E, V = () => string> and Property<E, V = string> So let's simplify the task to...

type Property<T> =
  | (T extends string | number | boolean | undefined ? T : never)
  | ((lastValue: T) => T)

function factory<T>(value: Property<T>): Property<T> {
  return value;
}

// Type 'string' is not assignable to type '() => string'.ts(2322)
factory(() => "")

type ExtractOrigin<P> = P extends Property<infer Origin> ? Origin : never;

type SecondGeneric = ExtractOrigin<() => string>
// We expect `string` but `string | (() => string)` comes back!

It looks like using type unions along with conditional types breaks typescript.

And your solution actually works for this case, although it looks absurd. I assume that after the conditional type, all other elements of the type union break down. image Therefore, conditional types need to be moved to the end of type unions throughout the code. I found several more such cases in the /** Store **/ section.

Qsppl commented 5 months ago

Sent a bug report: https://github.com/microsoft/TypeScript/issues/58016

Qsppl commented 5 months ago

Typescript contributors say that this problem will not be solved in the near future, so we are solving it ourselves.

smalluban commented 2 months ago

Do you have a solution, which might be implemented on our side? If not, I would close the issue.

Qsppl commented 2 months ago

The solution proposed by @au-z only works because if you have several incompatible candidates for output, then the first one is selected as the assumed type.

This solution fixes the problem in this specific use case, but may create typing errors in other use cases.

Typescript contributors will not fix this issue.

To completely solve the problem, we need to change the types significantly.

You can put this problem aside for later, I can solve it when more problems related to types appear.

smalluban commented 2 months ago

Thanks for clarification. Feel free to re-open if you think we can do something with it.