Skip to content

Improve type inference for generic curried functions #15016

Closed
@masaeedu

Description

@masaeedu
// #-------------- This works

type Repeat = (n: number) => (s: string) => string;
export const repeat: Repeat = n => s => s.repeat(n); // Sweet! s and n are both inferred!

// -------------------------#


// #----- Problems begin here

type Repeat2 = (n: number) => <T extends { repeat(n: number): T }>(repeatable: T) => T;

// Only n is inferred as number. Won't work with noImplicitAny enabled
export const repeat2Broken: Repeat2 = n => r => r.repeat(n); 
// Workaround is to redeclare everything from the type signature
export const repeat2Ugly: Repeat2 =
    n => <T extends { repeat(n: number): T }>(r: T) => r.repeat(n);

// -------------------------#


// #----------- It gets worse

// Type inference breaks for all args following the generic one, so even if you use
// the workaround, the next args won't regain inference
type Repeat3 = 
    (n: number) 
    => <T extends { repeat(n: number): T }>(repeatable: T) 
    => (reverse: boolean) 
    => T;

// Even if you explicitly provide the boilerplate for the generic arg, the boolean arg 
// won't be inferred. So the following won't work with noImplicitAny enabled
export const repeat3UglyAndBroken: Repeat3 =
    n => <T extends { repeat(n: number): T }>(r: T) => b => { ... }

// You have to add typing for the last arg
export const repeat3Uglier: Repeat3 =
    n => <T extends { repeat(n: number): T }>(r: T) => (b: boolean) => { ... }

// -------------------------#

I'd really appreciate better support for good inference in this kind of scenario, whether it is solved by supporting partial generic application or addition of participation of return-types in inference or whatever.

JS libraries that use a functional programming paradigm often end up loosely or any-ly typed, because you find yourself fighting the type system and adding repetitive type noise all over your code to derive any safety. A couple of examples where better support for FP would be useful:

  • Libraries like React or Apollo make use of curried higher-order functions for solving cross-cutting concerns. In-fact, my original use case comes from React higher-order components
  • Popular FP libraries like Ramda become tedious to use in a noImplicitAny project because any generic functions passed or received from the library tend to quickly devolve into {} unless you repeatedly hand-hold the type system

Here is my original use-case, if interested:

export type GetState<TState> = () => TState;
export type SetState<TState> = (newState: TState) => void;
export type StatelessComponent<TProp> = (prop: TProp) => React.ReactElement<TProp>
export type StatefulComponent<TProp, TState> = 
    (stateAccess: { getState: GetState<TState>, setState: SetState<TState> })
    => StatelessComponent<TProp>;

type ScrollableHOC = 
    (options: ScrollableOptions) 
    => <T extends ScrollableProps>(Child: ReactComponent<T>) 
    => StatefulComponent<T, ScrollableState>;

// The introduction of the Child arg breaks type inference for getState, setState, props
const scrollable: ScrollableHOC = opts => Child => ({ getState, setState }) => props => {
    // ...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions