Skip to content

buildFunctionIR maps CONSTRAINED type params to 'a, dropping the constraint — accepts code the library rejects #192

Description

@jagguji

Found during PR #189's review rounds. Pre-existing on main; not introduced by that PR, but it is now inconsistent with the method path that PR added.

The defect

buildFunctionIR (src/extract.mjs:~1443) maps every signature type parameter to a ReScript type variable, discarding the extends constraint:

export declare function fnConstrained<T extends string>(x: T): T;
@module("demo") external fnConstrained: ('a) => 'a = "fnConstrained"

'a accepts anything, so fnConstrained(42) compiles while TS rejects it. That is the same defect class as #177, pointed the other way: accepting code the library rejects.

The comment at the mapping site acknowledges the constraint is dropped, so this is a known simplification rather than an oversight — but it is unsound, and #189 declined to do the same thing for methods precisely on that ground.

The resulting asymmetry

After #189, the same TS shape gets opposite answers depending on whether it is a function or a method:

declaration binding
function text<T extends string>(t: T) ('a) => 'a — unsound, accepts 42
class C { text<T extends string>(t: T) } flagged string — honest

docs/TYPE_MAPPING.md's claim that method generics are mapped "like a standalone generic function's" therefore holds only for the unconstrained case.

Options

  1. Match the method path — leave a constrained param unmapped so it stays flagged. Honest and consistent, but coarse: it discards information the bound does provide.
  2. Resolve the boundT extends stringstring. Sound in the parameter position (at a call site T is inferred from the argument, so any string is admissible) and an honest ⚪ widening in a return position. This is the better answer, but it is currently blocked by Large numeric-literal unions generate unusable type names (v100OrV102Or…OrV511OrV1), blocking constrained-generic resolution #190: hono's U extends ContentfulStatusCode resolves to a ~60-member numeric union whose generated name is v100OrV102Or…OrV511OrV1, which then lands in every signature mentioning a status. Fix #177: two instantiations of one generic are two types (+ method generics become real type variables) #189 implemented and reverted exactly this for the same reason.

So the ordering is: fix #190's naming, then take option 2 in both paths and delete the asymmetry.

Also uncovered

Constructors and static methods still do not register signature type params at all, so static of<T>(x: T) stays a flagged string while the instance method gets 'a. Worth folding into whichever option is taken, so all four signature kinds agree.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions