Using object in a type does not work - 'unbound value'

Why doesn’t this work:

type t = {..} => unit

error:

A type variable is unbound in this type declaration.
In type {..} as 'a => unit the variable 'a is unbound

inference works:

defining it as a type - does not work:

(I don’t want to / cannot use type myFn<'a> = (int, 'a) => unit, because 'a is in practice unknown - it can be any object {..})

Playground Link

use

type myFn<'a> = (int, {..} as 'a) => unit
2 Likes

but notice this:

I don’t want to have to provide an 'a generic in each myFn consumer, because this value has no meaning - it can be any object {..}

Read the answer carefully, it is not (int, 'a) :wink:

what I mean is that when the type is type myFn<'a> you have to provide 'a in all places where you use the type myFn

but 'a has no meaning - I don’t want to provide it - I want to use simply the type myFn, not myFn<..>

it works for any type other that {..}, so I presume that {..} is doing some additional magic?

my (simplified) use case is that I want to specify the type in factories, resi files and etc. Relying on type inference works:

@send external doSomething: (int, {..}) => unit = "doSomething"
let myFn = (a, b) => doSomething(a, b)
let createSomething = () => {
  myFn
}

but specifying the type explicitly fails

{..} means any object so I assumed you want that.
You don’t have to supply 'a in that type,
in your example you can use myFn type like this:

  @send external doSomething: (int, {..}) => unit = "doSomething"
  let myFn = (a, b) => doSomething(a, b)

  type myFn<'a> = (int, {..} as 'a) => unit

  let createSomething: unit => myFn<_> = () => {
    myFn
  }

but I think you don’t want “any object” which is ofcourse a bit unsafe.
can you write an example in ts or other language so I can better understand what you actually want to do.

1 Like

I agree that any object is unsafe, but it is the use case I have - the keys are dynamic based on API responses and the values can be either a string or a function

The TypeScript version (which is more type-safe than the ReScript version I’m trying to achieve) is:

type Input = Record<string, string | number | ((x: string) => string)>

// @send external doSomething: (int, {..}) => unit = "doSomething"
const doSomething = (a: number, arg: Input): void => undefined

// type myFn<'a> = (int, {..} as 'a) => unit
type MyFn = (a: number, arg: Input) => void

// let myFn = (a, b) => doSomething(a, b)
const myFn: MyFn = (a, b) => doSomething(a, b)
  
/*
  let createSomething: unit => myFn<_> = () => {
    myFn
  }
*/
const createSomething: () => MyFn = () => myFn

I understand that in OCaml and ReScript you can have real arguments polymorphism and you can use discriminated unions / separate functions. But at the same time {..} seems to be a workaround that when it comes to interfacing with externals. My question is why {..} works as expected when defining an external:

@send external doSomething: (int, {..}) => unit = "doSomething"

and then it is inferred correctly when calling that external

let myFn = (a, b) => doSomething(a, b)

but it doesn’t work to define a type:

type myFn = (int, {..}) => unit

this is a more complete playground link that uses the suggestion, but unfortunately it still doesn’t work and fails ReScript Playground

module Config = {
  type t<'a>
  type factoryConfig<'a> = {value: 'a}
  @send external getConfig: factoryConfig<'value> => t<'a> = "getConfig"
}

module MyModule = {
  @send external doSomething: (int, {..}) => unit = "doSomething"
  type myFn<'a> = (int, 'a) => unit constraint 'a = {..}

  // works
  let config1: Config.t<int> = Config.getConfig({value: 5})

  // does not work
  let config2: Config.t<myFn<_>> = Config.getConfig({value: doSomething})
}

maybe i would write this as

type input = Js.Dict.t<InputValueShape.t>

where InputShape.t is

module InputValueShape = {
  type t
  type classified = String(string) | Number(float) | Fn(string => string)
  external unsafeToString: t => string = "%identity"
  external unsafeToNumber: t => float = "%identity"

  type strToStr = string => string
  external unsafeToFn: t => strToStr = "%identity"

  let classify = t =>
    switch Js.typeof(t) {
    | "function" => t->unsafeToFn->Fn->Some
    | "string" => t->unsafeToString->String->Some
    | "number" => t->unsafeToNumber->Number->Some
    | _ => None
    }
}
3 Likes

thanks @amiralies , appreciate the suggestions and effort
a small clarification - I want to call doSomething:

type Input = Record<string, string | number | ((x: string) => string)>
const doSomething = (a: number, arg: Input): void => undefined

depending on runtime information I have to call it with:

doSomething(1, { "arg1": 1, "arg2: x => x.toUpperCase() })

or

doSomething(1, { "arg3": "1" })

and etc. So I’m looking for a zero-runtime cost to provide this Input arg - {..} is not perfect, but it did just fine

The issue with Js.Dict.t is that you have to create it “indirectly”, for example with fromArray or fromList - you can’t build it with 0 cost. There’s also additional runtime cost for converting from discriminated union - I would need to have something like:

  module MyModule = {
    type t
    type arg = String(string) | Int(int) | Fn(string => string)
    @val external doSomething: (int, Js.Dict.t<t>) => unit = "doSomething"

    external unsafeOfString: string => t = "%identity"
    external unsafeOfInt: int => t = "%identity"
    external unsafeOfFn: (string => string) => t = "%identity"
    let doSomething = (a, b: array<(string, arg)>) => {
      let b = Js.Array2.map(b, ((k, v)) => {
        let v = switch v {
        | String(x) => unsafeOfString(x)
        | Int(x) => unsafeOfInt(x)
        | Fn(x) => unsafeOfFn(x)
        }
        (k, v)
      })
      doSomething(a, Js.Dict.fromArray(b))
    }
  }

that’s a lot of generated runtime code for something that should be 0 cost (and is with the TypeScript snippet above), so I’m hoping to find something even suboptimal, but 0 cost

Js.Dict.t is a mutable js obejct, you can use empty() to create an instance and use set to add pairs to it.

https://rescript-lang.org/try?code=DYUwLgBAHhC8EG8BQEIHsAOIB2EBSAzgHQAiAlgMZgoSiQAmlk8IAthmAJ4AUAlDYyoBaAHwFw3AEQAzNGkkAaCACZ+qQdQC+SIA

I introduced variants to make the type ergonmic you may use typeof and conversion functions on usage site if you want zero runtime.

1 Like

thanks amiralies, there is definetely less code generated:
old doSomething, mutation doSomething2, but I’m hoping to make doSomething3 work (which is less type safe, but is 0 cost):

Playground link

1 Like