Repository navigation
Sub-optimal type parameter inference with strictFunctionTypes enabled #52111
Copy link
Copy link
Closed
Labels
BugA bug in TypeScriptA bug in TypeScriptFix AvailableA PR has been opened for this issueA PR has been opened for this issue
Milestone
Description
These are cases I've extracted from #49929; they are cases where if you don't specify the type parameters yourself, the inference isn't good.
This is potentially a full duplicate of #49924, however, the fix suggested in that issue does not work properly in our codebase. To test these, it may be easiest to just clone #49929 with the explicit type parameters removed and see what works and what doesn't.
enum SyntaxKind {
Block,
Identifier,
CaseClause,
FunctionExpression,
FunctionDeclaration,
}
interface Node { kind: SyntaxKind; }
interface Expression extends Node { _expressionBrand: any; }
interface Declaration extends Node { _declarationBrand: any; }
interface Block extends Node { kind: SyntaxKind.Block; }
interface Identifier extends Expression, Declaration { kind: SyntaxKind.Identifier; }
interface CaseClause extends Node { kind: SyntaxKind.CaseClause; }
interface FunctionDeclaration extends Declaration { kind: SyntaxKind.FunctionDeclaration; }
type HasLocals = Block | FunctionDeclaration;
declare function canHaveLocals(node: Node): node is HasLocals;
declare function assertNode<T extends Node, U extends T>(node: T | undefined, test: (node: T) => node is U): asserts node is U;
declare function assertNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined): void;
function foo(node: FunctionDeclaration | CaseClause) {
assertNode(node, canHaveLocals)
node;
// ^?
assertNode<Node, HasLocals>(node, canHaveLocals)
node;
// ^?
}
declare function isExpression(node: Node): node is Expression;
declare function cast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut;
function bar(node: Identifier | FunctionDeclaration) {
const a = cast(node, isExpression);
// ^?
const b = cast<Expression>(node, isExpression);
// ^?
}Output
"use strict";
var SyntaxKind;
(function (SyntaxKind) {
SyntaxKind[SyntaxKind["Block"] = 0] = "Block";
SyntaxKind[SyntaxKind["Identifier"] = 1] = "Identifier";
SyntaxKind[SyntaxKind["CaseClause"] = 2] = "CaseClause";
SyntaxKind[SyntaxKind["FunctionExpression"] = 3] = "FunctionExpression";
SyntaxKind[SyntaxKind["FunctionDeclaration"] = 4] = "FunctionDeclaration";
})(SyntaxKind || (SyntaxKind = {}));
function foo(node) {
assertNode(node, canHaveLocals);
node;
// ^?
assertNode(node, canHaveLocals);
node;
// ^?
}
function bar(node) {
const a = cast(node, isExpression);
// ^?
const b = cast(node, isExpression);
// ^?
}Compiler Options
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"alwaysStrict": true,
"esModuleInterop": true,
"declaration": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2017",
"jsx": "react",
"module": "ESNext",
"moduleResolution": "node"
}
}Playground Link: Provided
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugA bug in TypeScriptA bug in TypeScriptFix AvailableA PR has been opened for this issueA PR has been opened for this issue
Activity
ahejlsberg commentedon Jan 5, 2023
I think everything works if you slightly tweak my suggestion here to instead introduce an overload with three type parameters to handle cases where the input and output types share a supertype but aren't otherwise related:
Here's a modified example where everything appears to work as expected.
jakebailey commentedon Jan 5, 2023
This is the workaround I mentioned that I tried and reverted; if you look at that playground, you'll see the assertion function has an error on its return because it doesn't like the relationship of the input and output.
The change in cast also has the unfortunate side effect of letting you cast anything to anything else, since it'll happily choose unknown/any as a supertype:
Which is "fine" because it'll just throw, but does mean you can't know if you're attempting to cast to a totally unrelated type.
ahejlsberg commentedon Jan 6, 2023
First a simplified example:
Currently, the
cast(b, isC)call fails because we inferBfor bothTandU. In cases where we have inferences from both co- and contra-variant positions, we prefer the co-variant inference when it is a subtype of the contra-variant inference (i.e. when it is more specific). In the example we have two inference candidates,AandB, forT, and we chooseBbecause it is more specific thanA. However, we also have a requirement that our inference forUmust extend our inference forT, but we fail to consider that as we're choosing. In the example,CextendsA, but notB, so reallyAis the only meaningful inference forT.I'm going to put up a PR that adds this extra consideration to type inference.