I found the proper way to do this:
My error was using a record as an interface, but if I declare a full interface for the Request then it works fine.
In this Example, now the Record just has a field that is the type of the interface:
module Example1 =
type IProps =
abstract build: string -> unit
type IReq =
abstract Req: IProps
type PropsA =
{ a: string }
interface IProps with
member this.build _ = ()
type Request =
{ Req: IProps }
interface IReq with
member this.Req = this.Req :> IProps
// type Request = Request<IProps> <-- not possible, not necesary
// let fn (x: Request<PropsA>): IReq = <-- not possible
let fn (x: Request): IReq =
// It works
x :> IReq
{ Req = { a = "" } }
|> fun x -> x.Req.a // <-- but here Req is IProps, so I cannot accesss the concrete type PropsA
|> ignore
but it makes it too generic, another try:
module Example2 =
type IProps =
abstract build: string -> unit
type IReq =
abstract Req: IProps
type PropsA =
{ a: string }
interface IProps with
member this.build _ = ()
type Request<'a when 'a :> IProps> =
{ Req: 'a }
interface IReq with
member this.Req = this.Req :> IProps
type Request = Request<IProps> // <-- Possible, but not necessary
let fn (x: Request<PropsA>): Request =
// Type constraint mismatch. The type 'Request<PropsA>' is not compatible with type 'Request'
x :> Request
let fn2 (x: Request<PropsA>): IReq = // <-- notice the type alias is not needed, as now IReq is a proper interface
// It works, proper way to do it.
x :> IReq
{ Req = { a = "" } }
|> fun x -> x.Req.a // Works, Req:: PropsA
|> ignore
Now Record<PropsA> is a full concrete type that is compatible with IReq and casting just works ™️, this allows the use of flexible types and the rest of generic capabilities to work as expected, I was under the misconception that a generic record would behave as an interface.
Another more specialized version that is also possible is:
module Example3 =
type IProps =
abstract build: string -> unit
type IReq<'a when 'a :> IProps> =
abstract Req: 'a
type PropsA =
{ a: string }
interface IProps with
member this.build _ = ()
type Request<'a when 'a :> IProps> =
{ Req: 'a }
interface IReq<'a> with
member this.Req = this.Req // <-- notice the cast is no longer needed
type Request = Request<IProps> // <-- Possible, but not necessary
let fn2 (x: Request<PropsA>): IReq<_> =
// It works, proper way to do it.
x :> IReq<_>
{ Req = { a = "" } }
|> fun x -> x.Req.a // Works, Req:: PropsA
|> ignore
The benefit of this version is that the cast is not needed and gives more flexibility to IReq.