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.
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");
... 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
Most helpful comment
I feel like I'm missing something, because this example really shouldn't error, but has since 2.8