Typescript: Type inference issue when using generic function and generic constraint

Created on 7 Jun 2018  路  7Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.0.0-dev.20180607 and 2.9.1


Search Terms: type inference generic constraint function callback accessor

Code

type GenericFunc<TA, TB> = (x: TA) => TB;

class GenericFuncHolder<TA, TB> {
    constructor(readonly func: GenericFunc<TA, TB>) { }
}

function wrap<TA, TB>(func: GenericFunc<TA, TB>) { return new GenericFuncHolder(func); }

class X { readonly xProperty = "xProperty"; }

class Y<TX> { readonly x: TX | undefined; readonly yProperty = "yProperty"; }
class YDerived<TX> extends Y<TX> { readonly yDerivedProperty = "yDerivedProperty"; }

class GenericTarget<TX, TY> {

    constructor(readonly x: TX, readonly y: TY) { }

    holderParams<TX2, TY2 extends Y<TX2>>(
        holderX: GenericFuncHolder<TX, TX2>, holderY: GenericFuncHolder<TY, TY2>,
        use: (x: TX2, y: TY2) => void,
    ) {
        use(holderX.func(this.x), holderY.func(this.y));
    }

    funcParamsWithoutConstraint<TX2, TY2>(
        funcX: GenericFunc<TX, TX2>, funcY: GenericFunc<TY, TY2>,
        use: (x: TX2, y: TY2) => void,
    ) {
        use(wrap(funcX).func(this.x), wrap(funcY).func(this.y));
    }

    funcParamsWithConstraint_BAD<TX2, TY2 extends Y<TX2>>(
        funcX: GenericFunc<TX, TX2>, funcY: GenericFunc<TY, TY2>,
        use: (x: TX2, y: TY2) => void,
    ) {
        use(wrap(funcX).func(this.x), wrap(funcY).func(this.y));
    }
}


const genericClassTarget = new GenericTarget (new X(), new Y<X>());

// Type inference works when having the constraint but using `wrap()` on the call site:
genericClassTarget.holderParams(wrap(x => new X()), wrap(y => new YDerived<X>()),
    (x2, y2) => console.log(x2.xProperty, y2.yDerivedProperty));

// Type inference works without the `TY2` constraint:
genericClassTarget.funcParamsWithoutConstraint(x => new X(), y => new YDerived<X>(),
    (x2, y2) => console.log(x2.xProperty, y2.yDerivedProperty));

// Type inference doesn't work, TSC emits "TS2339: Property 'xProperty' does not exist on type '{}'"
genericClassTarget.funcParamsWithConstraint_BAD(x => new X(), y => new YDerived<X>(),
    (x2, y2) => console.log(x2.xProperty, y2.yDerivedProperty));

(Sorry for the long repro, it's the simplified version of a production issue and the inference issue seems to be a very special case.)

Expected behavior:
Type inference works for funcParamsWithConstraint_BAD as it works for holderParams and funcParamsWithoutConstraint. It seems that the generic constraint of TY2 narrows TX2 to {}, but this works when using wrap() on the call site. Which is the reason, why I would expect funcParamsWithConstraint_BAD to work just fine.

Actual behavior:
"TS2339: Property 'xProperty' does not exist on type '{}'" in the last line.

Playground Link: Playground Link

EDIT: Extended repro to make it more realistic.

Design Limitation Rescheduled

Most helpful comment

I feel like I'm missing something, because this example really shouldn't error, but has since 2.8

type TypeName<T> = T extends number ? "number" : "something else";
function fn<T>(func: (x: string) => T, p: TypeName<T>): void { }
// Error
fn(x => x.length, "number");

All 7 comments

TY2 in _BAD isn't doing anything. The signature should be written as:

funcParamsWithConstraint_BAD<TX2>(
    funcX: GenericFunc<TX, TX2>, funcY: GenericFunc<TY, Y<TX2>>,
    useX2: (x: TX2) => void,
) {
    wrap(funcX); wrap(funcY);
}

This works as expected.

@RyanCavanaugh Okay, maybe I oversimplified the repro sample. I updated it (now indeed using TY2).

The real code is a fluent API, where I need the type inference and the TY2 extends Y<TX2> constraint.

@RyanCavanaugh Also added YDerived to motivate TY2 better.

@RyanCavanaugh Any news on this?

I have now a second but much easier sample using a conditional type instead of a generic constraint:

type GenericFunc<TA, TB> = (x: TA) => TB;

class GenericFuncHolder<TA, TB> {
    constructor(readonly func: GenericFunc<TA, TB>) { }
}

function wrap<TA, TB>(func: GenericFunc<TA, TB>) { return new GenericFuncHolder(func); }

//

export type PropertyToTypeName<TProperty> =
    TProperty extends string | undefined ? "text" :
        TProperty extends number | undefined ? "number" :
            never;

function withFunc<TProperty>(
    func: GenericFunc<string, TProperty>, typeName: PropertyToTypeName<TProperty>) {
    console.log('func points to', typeName);
}

function withHolder<TProperty>(
    holder: GenericFuncHolder<string, TProperty>, typeName: PropertyToTypeName<TProperty>) {
    console.log('holder points to', typeName);
}


withHolder(wrap(x => x.length), "number"); // works fine

// emits TS2345: Argument of type '"number"' is not assignable to parameter of type 'never'
withFunc(x => x.length, "number");

Playground link

... like in the sample above the problem is that in the withFunc-case TProperty infers to {} which you can see when adding TProperty extends {} ? "[is empty object]" to the conditional type.

One more observation: When adding TProperty extends string | number constraints in the second example it works (also for withFunc). It seems that the inference works correctly because it cannot infer down to {}.

I feel like I'm missing something, because this example really shouldn't error, but has since 2.8

type TypeName<T> = T extends number ? "number" : "something else";
function fn<T>(func: (x: string) => T, p: TypeName<T>): void { }
// Error
fn(x => x.length, "number");

The sample above now produces a crash in the LS in the Playground...

Uncaught Error: Cannot read property 'kind' of undefined

TypeError: Cannot read property 'kind' of undefined
    at tE (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at Mh (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at Object.getTypeAtLocation (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at i (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at Object.getCodeActions (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7
    at Object.f.flatMap (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at Object.e.getFixes (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7
    at Object.f.flatMap (unpkg.com/@typescript-deploys/[email protected]/min/vs/language/typescript/tsWorker.js:7)
    at editor.main.js:38
Was this page helpful?
0 / 5 - 0 ratings