oyvindberg / st-material-ui

Material UI 5 for Scala 3 (Slinky and Scalajs-React)
32 stars 2 forks source link

For more ergonomics, Scala3's type inference mechanism can be used #4

Open elgca opened 3 weeks ago

elgca commented 3 weeks ago

In Scala3 we have a type inference system, that makes the mui facades more ergonomic. Its looks like this

type Elem[X] = X match
  case String => Char
  case Array[t] => t
  case Iterable[t] => t

https://docs.scala-lang.org/scala3/reference/new-types/match-types.html

I used it to rewrite Autocomplete the onChange call cannot get the exact type of value,onChange event you current value type define it as follows:

  /** NOTE: Conditional type definitions are impossible to translate to Scala.
    * See https://www.typescriptlang.org/docs/handbook/2/conditional-types.html for an intro.
    * This RHS of the type alias is guess work. You should cast if it's not correct in your case.
    * TS definition: {{{
    Multiple extends true ? std.Array<T | @mui/base.@mui/base/useAutocomplete/useAutocomplete.AutocompleteFreeSoloValueMapping<FreeSolo>> : DisableClearable extends true ? T | @mui/base.@mui/base/useAutocomplete/useAutocomplete.AutocompleteFreeSoloValueMapping<FreeSolo> : T | null | @mui/base.@mui/base/useAutocomplete/useAutocomplete.AutocompleteFreeSoloValueMapping<FreeSolo>
    }}}
    */
  type AutocompleteValue[T, Multiple, DisableClearable, FreeSolo] = js.Array[T | AutocompleteFreeSoloValueMapping[FreeSolo]]

image

If we use type match for type inference,we can get like this:

image

How it works

The first is to provide type definitions, as follows, this is a set of type match types that can be from a given type inference what we want

  /* For automatic type inference, Scala's Boolean is not possible; it must be explicitly indicated that the value can
   * only be true or false. */
  type Bool = true | false

  // export type AutocompleteFreeSoloValueMapping<FreeSolo> = FreeSolo extends true ? string : never;
  // type FreeSoloValue[FreeSolo] = FreeSolo match
  //   case true => String
  //   case _    => FreeSoloValue[FreeSolo]
  // I don't know what the effect of never is,
  // logically I choose the following implementation,
  // using the above logic when FreeSolo =:= false can trigger a compilation error,
  // when using above it to `T | FreeSoloValue[FreeSolo]` not FreeSoloValue[T, FreeSolo]
  type FreeSoloValue[T, FreeSolo] = FreeSolo match
    case true => T | String
    case _    => T

  // export type AutocompleteValue<Value, Multiple, DisableClearable, FreeSolo> = Multiple extends true
  //   ? Array<Value | AutocompleteFreeSoloValueMapping<FreeSolo>>
  //   : DisableClearable extends true
  //     ? NonNullable<Value | AutocompleteFreeSoloValueMapping<FreeSolo>>
  //     : Value | null | AutocompleteFreeSoloValueMapping<FreeSolo>;
  type MyAutocompleteValue[T, Multiple, DisableClearable, FreeSolo] = Multiple match
    case true => js.Array[FreeSoloValue[T, FreeSolo]]
    case _    =>
      DisableClearable match
        case true => FreeSoloValue[T, FreeSolo]
        case _    => FreeSoloValue[T, FreeSolo] | Unit

And then changed the type definition of the Builder and onChange

open class Builder[T <: js.Any, Multiple <: Bool, DisableClearable <: Bool, FreeSolo <: Bool](val args: js.Array[Any])

def onChange(
  value: (
    /* event */ ReactEventFrom[org.scalajs.dom.Element],
    /* value */ MyAutocompleteValue[
      T,
      Multiple,
      DisableClearable,
      FreeSolo,
    ], /* reason */ AutocompleteChangeReason, /* details */ js.UndefOr[AutocompleteChangeDetails[T]]) => Callback,
): this.type

and in order for the types to be correctly inferred, I made sure that the types of Multiple,DisableClearable, and FreeSolo were explicit when using 'onChange'

// Multiple 
inline def multiple[M2 <: Bool](value: M2): Builder[T, M2, DisableClearable, FreeSolo] =
      set("multiple", value.asInstanceOf[js.Any]).asInstanceOf[Builder[T, M2, DisableClearable, FreeSolo]]
// DisableClearable
inline def disableClearable[DisableClearable2 <: Bool](value: DisableClearable2): Builder[T, Multiple, DisableClearable2, FreeSolo] =
  set("disableClearable", value.asInstanceOf[js.Any])
    .asInstanceOf[Builder[T, Multiple, DisableClearable2, FreeSolo]]
// FreeSolo
inline def freeSolo[FreeSolo2 <: Bool](value: FreeSolo2): Builder[T, Multiple, DisableClearable, FreeSolo2] =
  set("freeSolo", value.asInstanceOf[js.Any])
    .asInstanceOf[Builder[T, Multiple, DisableClearable, FreeSolo2]]

Of course, these types have default values, and apply needs to be redefined

inline def apply[T <: js.Any](
  options: js.Array[T],
  renderInput: AutocompleteRenderInputParams => Node): Builder[T, false, false, false] = {
  val __props =
    js.Dynamic.literal(options = options.asInstanceOf[js.Any], renderInput = js.Any.fromFunction1(renderInput))
  new Builder[T, false, false, false](
    js.Array(this.component, __props.asInstanceOf[AutocompleteProps[T, false, false, false, "div"]]))
}

Now, when calling any method that changes the 'Multiple', 'DisableClearable', and 'FreeSolo' types, onChange can now automatically inference the appropriate type。 .multiple(true)change Multiple to ture(as a type)

image

elgca commented 3 weeks ago

Note: onChange should not have the inline keyword before, maybe it's a bug in scala

oyvindberg commented 3 weeks ago

Thanks for the proposal!

It's a bit complicated honestly. I'm not going to build this into ST, because it's a huge effort and very low frequency.

I think the logical thing to do here is to add a manually written, customized version of the generated code. Inline the different states, and expose different components for what has the same physical js implementation.

I don't think it should use match types, since those don't work in intellij. More explicit is better here, honestly.

I'd gladly accept such a manually tweaked facade

elgca commented 3 weeks ago

I just tried it on IntelliJ, it doesn't work. scala3 is still not well supported😭 Yes, it's a huge effort, if the code cannot be auto generated.

oyvindberg commented 3 weeks ago

No, it's not a huge effort. This is a very rare typescript pattern, and you can perfectly model the few usecases in mui with copy/paste+customization

elgca commented 3 weeks ago

Thanks. If this typescript type is rare, that's good news.Manually modify it when I need to.I've designed new type inference to make this work seem easier

  type TRUE    = true
  type FALSE   = false
  type BOOLEAN = TRUE | FALSE

  sealed trait Match
  object NotMatch extends Match
  object Matched  extends Match

  type ??[A, B] = A match
    case TRUE  => B
    case FALSE => NotMatch.type
    case _     => ??[A, B] // for a compile error

  type ::[A, B] = A match
    case NotMatch.type => B
    case _             => A

  // export type AutocompleteFreeSoloValueMapping<FreeSolo> = FreeSolo extends true ? string : never;
  type FreeSoloValue[T, FreeSolo] = FreeSolo ?? (T | String) :: T

  // export type AutocompleteValue<Value, Multiple, DisableClearable, FreeSolo> = Multiple extends true
  //   ? Array<Value | AutocompleteFreeSoloValueMapping<FreeSolo>>
  //   : DisableClearable extends true
  //     ? NonNullable<Value | AutocompleteFreeSoloValueMapping<FreeSolo>>
  //     : Value | null | AutocompleteFreeSoloValueMapping<FreeSolo>;
  type AutocompleteValue[T, Multiple, DisableClearable, FreeSolo] =
    Multiple ?? (js.Array[FreeSoloValue[T, FreeSolo]]) ::
      (DisableClearable ?? FreeSoloValue[T, FreeSolo] :: (FreeSoloValue[T, FreeSolo] | Unit))