Typescript: ramda broken by #33228, "less aggressive contextual signature instantiation"

Created on 23 Sep 2019  路  7Comments  路  Source: microsoft/TypeScript

  1. clone dt
  2. cd ~/dt/types/ramda
  3. in ramda-tests.ts, look for (or add) the following code:
const x: number = R.thunkify(R.identity)(42)();

Expected behavior:
No error?

Actual behavior:
Error, 'unknown' is not assignable to 'number'.

Reverting #33228 fixes the error.

Working as Intended

Most helpful comment

Who would have thunk that the solution to a problem with generics would be more generics?

(I only came here for the joke, sorry)

All 7 comments

Here is a 'standalone' repro, in which I copied all the relevant types from Ramda:

type IterationMap = {
    '-40': ['__', '-39', '-40', -40, '-'];
    '-39': ['-40', '-38', '-39', -39, '-'];
    '-38': ['-39', '-37', '-38', -38, '-'];
    '-37': ['-38', '-36', '-37', -37, '-'];
    '-36': ['-37', '-35', '-36', -36, '-'];
    '-35': ['-36', '-34', '-35', -35, '-'];
    '-34': ['-35', '-33', '-34', -34, '-'];
    '-33': ['-34', '-32', '-33', -33, '-'];
    '-32': ['-33', '-31', '-32', -32, '-'];
    '-31': ['-32', '-30', '-31', -31, '-'];
    '-30': ['-31', '-29', '-30', -30, '-'];
    '-29': ['-30', '-28', '-29', -29, '-'];
    '-28': ['-29', '-27', '-28', -28, '-'];
    '-27': ['-28', '-26', '-27', -27, '-'];
    '-26': ['-27', '-25', '-26', -26, '-'];
    '-25': ['-26', '-24', '-25', -25, '-'];
    '-24': ['-25', '-23', '-24', -24, '-'];
    '-23': ['-24', '-22', '-23', -23, '-'];
    '-22': ['-23', '-21', '-22', -22, '-'];
    '-21': ['-22', '-20', '-21', -21, '-'];
    '-20': ['-21', '-19', '-20', -20, '-'];
    '-19': ['-20', '-18', '-19', -19, '-'];
    '-18': ['-19', '-17', '-18', -18, '-'];
    '-17': ['-18', '-16', '-17', -17, '-'];
    '-16': ['-17', '-15', '-16', -16, '-'];
    '-15': ['-16', '-14', '-15', -15, '-'];
    '-14': ['-15', '-13', '-14', -14, '-'];
    '-13': ['-14', '-12', '-13', -13, '-'];
    '-12': ['-13', '-11', '-12', -12, '-'];
    '-11': ['-12', '-10', '-11', -11, '-'];
    '-10': ['-11', '-9', '-10', -10, '-'];
    '-9': ['-10', '-8', '-9', -9, '-'];
    '-8': ['-9', '-7', '-8', -8, '-'];
    '-7': ['-8', '-6', '-7', -7, '-'];
    '-6': ['-7', '-5', '-6', -6, '-'];
    '-5': ['-6', '-4', '-5', -5, '-'];
    '-4': ['-5', '-3', '-4', -4, '-'];
    '-3': ['-4', '-2', '-3', -3, '-'];
    '-2': ['-3', '-1', '-2', -2, '-'];
    '-1': ['-2', '0', '-1', -1, '-'];
    '0': ['-1', '1', '0', 0, '0'];
    '1': ['0', '2', '1', 1, '+'];
    '2': ['1', '3', '2', 2, '+'];
    '3': ['2', '4', '3', 3, '+'];
    '4': ['3', '5', '4', 4, '+'];
    '5': ['4', '6', '5', 5, '+'];
    '6': ['5', '7', '6', 6, '+'];
    '7': ['6', '8', '7', 7, '+'];
    '8': ['7', '9', '8', 8, '+'];
    '9': ['8', '10', '9', 9, '+'];
    '10': ['9', '11', '10', 10, '+'];
    '11': ['10', '12', '11', 11, '+'];
    '12': ['11', '13', '12', 12, '+'];
    '13': ['12', '14', '13', 13, '+'];
    '14': ['13', '15', '14', 14, '+'];
    '15': ['14', '16', '15', 15, '+'];
    '16': ['15', '17', '16', 16, '+'];
    '17': ['16', '18', '17', 17, '+'];
    '18': ['17', '19', '18', 18, '+'];
    '19': ['18', '20', '19', 19, '+'];
    '20': ['19', '21', '20', 20, '+'];
    '21': ['20', '22', '21', 21, '+'];
    '22': ['21', '23', '22', 22, '+'];
    '23': ['22', '24', '23', 23, '+'];
    '24': ['23', '25', '24', 24, '+'];
    '25': ['24', '26', '25', 25, '+'];
    '26': ['25', '27', '26', 26, '+'];
    '27': ['26', '28', '27', 27, '+'];
    '28': ['27', '29', '28', 28, '+'];
    '29': ['28', '30', '29', 29, '+'];
    '30': ['29', '31', '30', 30, '+'];
    '31': ['30', '32', '31', 31, '+'];
    '32': ['31', '33', '32', 32, '+'];
    '33': ['32', '34', '33', 33, '+'];
    '34': ['33', '35', '34', 34, '+'];
    '35': ['34', '36', '35', 35, '+'];
    '36': ['35', '37', '36', 36, '+'];
    '37': ['36', '38', '37', 37, '+'];
    '38': ['37', '39', '38', 38, '+'];
    '39': ['38', '40', '39', 39, '+'];
    '40': ['39', '__', '40', 40, '+'];
    '__': ['__', '__', string, number, '-' | '0' | '+'];
};

type Match = 'default' | 'implements->' | '<-implements' | 'extends->' | '<-extends' | 'equals';

export declare type Way = '->' | '<-';
export declare type Index = string | number | symbol;
export declare type Keys<O extends object> = keyof O & Index;

export declare type True = 1;
export declare type False = 0;
export declare type Extends<A1 extends any, A2 extends any> = [A1] extends [never] ? False : A1 extends A2 ? True : False;
export declare type Implements<A1 extends any, A2 extends any> = Extends<A1, A2> extends True ? True : False;
export declare type Equals<A1 extends any, A2 extends any> = (<A>() => A extends A1 ? True : False) extends (<A>() => A extends A2 ? True : False) ? True : False;
export declare type Is<A extends any, A1 extends any, match extends Match = 'default'> = {
    'default': Extends<A, A1>;
    'implements->': Implements<A, A1>;
    'extends->': Extends<A, A1>;
    '<-implements': Implements<A1, A>;
    '<-extends': Extends<A1, A>;
    'equals': Equals<A1, A>;
}[match];
type SelectKeys<O extends object, M extends any, match extends Match = 'default'> = {
    [K in Keys<O>]: {
        1: K;
        0: never;
    }[Is<O[K], M, match>];
}[Keys<O>];
declare type KnownIterationMapKeys = Exclude<keyof IterationMap, '__'>;
declare type PositiveIterationKeys = SelectKeys<IterationMap, [any, any, any, any, '+']>;
declare type NegativeIterationKeys = SelectKeys<IterationMap, [any, any, any, any, '-']>;
export declare type Numbers = {
    'string': {
        'all': Format<IterationMap[KnownIterationMapKeys], 's'>;
        '+': Format<IterationMap[PositiveIterationKeys], 's'>;
        '-': Format<IterationMap[NegativeIterationKeys], 's'>;
        '0': Format<IterationMap['0'], 's'>;
    };
    'number': {
        'all': Format<IterationMap[KnownIterationMapKeys], 'n'>;
        '+': Format<IterationMap[PositiveIterationKeys], 'n'>;
        '-': Format<IterationMap[NegativeIterationKeys], 'n'>;
        '0': Format<IterationMap['0'], 'n'>;
    };
};
type IterationOf<N extends string> = N extends keyof IterationMap ? IterationMap[N] : IterationMap['__'];
type Iteration = [keyof IterationMap, keyof IterationMap, string, number, '-' | '0' | '+'];
type Pos<I extends Iteration> = Format<I, 'n'>;
type Format<I extends Iteration, fmt extends 's' | 'n'> = {
    's': I[2];
    'n': I[3];
}[fmt];
// redeclare some built-in types
type Number = string;
type Function<P extends any[] = any, R extends any = any> = (...args: P) => R;
type Return<F extends Function> = F extends ((...args: any[]) => infer R) ? R : never;

type Prepend<T extends any[], A extends any> = ((head: A, ...args: T) => any) extends ((...args: infer U) => any) ? U : T;
export declare type Key<I extends Iteration> = Format<I, 's'>;
type NumberOf<N extends number> = N extends Numbers['number']['all'] ? Key<IterationMap[SelectKeys<IterationMap, [any, any, any, N, any]>]> : string;
type Length<T extends any[], fmt extends 's' | 'n' = 'n'> = {
    's': NumberOf<T['length']>;
    'n': T['length'];
}[fmt];

export declare type Next<I extends Iteration> = IterationMap[I[1]];
type _Reverse<T extends any[], TO extends any[] = [], I extends Iteration = IterationOf<'0'>> = {
    0: _Reverse<T, Prepend<TO, T[Pos<I>]>, Next<I>>;
    1: TO;
}[Pos<I> extends Length<T> ? 1 : 0];
type Reverse<T extends any[], TO extends any[] = []> = _Reverse<T, TO> extends infer X ? Cast<X, any[]> : never;

type Tail<T extends any[]> = ((...t: T) => any) extends ((head: any, ...tail: infer TTail) => any) ? TTail : never;
type Concat<T extends any[], T1 extends any[]> = Reverse<Reverse<T>, T1>;
type Append<T extends any[], A extends any> = Concat<T, [A]>;

type _Drop<T extends any[], N extends Number, I extends Iteration = IterationOf<'0'>> = {
    0: _Drop<Tail<T>, N, Next<I>>;
    1: T;
}[N extends Key<I> ? 1 : 0];
type Drop<T extends any[], N extends Number, way extends Way = '->'> = {
    '->': _Drop<T, N>;
    '<-': Reverse<Drop<Reverse<T>, N>>;
}[way] extends infer X ? Cast<X, any[]> : never;

export declare type Type<A extends any, Id extends string> = A & {
    __type: Id;
};
export declare type x = Type<'x', 'x'>;
type GapOf<T1 extends any[], T2 extends any[], TN extends any[], I extends Iteration = IterationOf<'0'>> = T1[Pos<I>] extends x ? Append<TN, T2[Pos<I>]> : TN;
type _GapsOf<T1 extends any[], T2 extends any[], TN extends any[] = [], I extends Iteration = IterationOf<'0'>> = {
    0: _GapsOf<T1, T2, GapOf<T1, T2, TN, I>, Next<I>>;
    1: Concat<TN, Drop<T2, Key<I>>>;
}[Pos<I> extends Length<T1> ? 1 : 0];
type GapsOf<T1 extends any[], T2 extends any[]> = _GapsOf<T1, T2> extends infer X ? Cast<X, any[]> : never;
type Gaps<T extends any[]> = NonNullable<{
    [K in keyof T]?: T[K] | x;
}>;
type Cast<A1 extends any, A2 extends any> = A1 extends A2 ? A1 : A2;
type Curry<F extends Function> = <T extends any[]>(...args: Cast<T, Gaps<Parameters<F>>>) => GapsOf<T, Parameters<F>> extends [any, ...any[]] ? Curry<(...args: GapsOf<T, Parameters<F>>) => Return<F>> : Return<F>;
declare function identity<T>(a: T): T;
declare function thunkify<F extends (...args: any[]) => any>(fn: F): Curry<(...args: Parameters<F>) => (() => ReturnType<F>)>;
const x: number = thunkify(identity)(42)();

Note that this repro has an additional error, so it's probably not quite right.

It comes down to the difference between these:

type S0 = Parameters<(x: any) => any>;  // [any]
type S1 = ReturnType<(x: any) => any>;  // any

type T0 = Parameters<<T>(x: T) => T>;  // [unknown]
type T1 = ReturnType<<T>(x: T) => T>;  // unknown

Previously we'd instantiate the generic function R.identity in the context of the constraint type (...args: any[]) => any but now we leave the generic function type alone. Neither behavior is particularly great, but unknown is certainly a safer outcome than any and the error in the test now calls attention to the fact that thunkify doesn't preserve generics. Previously the any types would completely circumvent checking and allow you to pass anything. So, I think the current behavior is preferable.

@pirix-gh I think I'll put the actual type (unknown) in the tests for now until you can look at preservation of generics in thunkify.

Based on @ahejlsberg鈥檚 observation, this is essentially similar to the monomorphism restriction, I think, and is not particularly surprising from a theoretical standpoint. The generic has to be resolved to a concrete function type when passed to R.thunkify, but there鈥檚 nothing to infer yet so you get unknown. Could be solved with a Rank 2 overload on Ramda鈥檚 end (i.e. in layman鈥檚 terms the function parameter of R.thunkify could itself be made generic).

Who would have thunk that the solution to a problem with generics would be more generics?

(I only came here for the joke, sorry)

@sandersn, I don't think I'll be able to preserve the generics in this case. I would need to be able to have some kind of reference to the template/generic types. This is the biggest limitation of Curry and other types I provide (at least for now).

However, the previous behavior could be fixed like this:

thunkify<P extends any[], R>(fn: (...args: P) => R): F.Curry<(...args: P) => (() => R)>;

To me, this is working as intended, and it makes sense that it returns unknown because any was transparently failing. But it could be greatly improved with your proposal for variadic kinds and/or type references thanks to the way they'd defer the generics' evaluation (resolves to unknown immediately atm). And this would mean lots and exciting possibilities for TypeScript (& fp).

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

blendsdk picture blendsdk  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

wmaurer picture wmaurer  路  3Comments

jbondc picture jbondc  路  3Comments