Skip to content

Inferred type parameter is too narrow with strictFunctionTypes enabled and branded types #49924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jakebailey opened this issue Jul 15, 2022 · 3 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@jakebailey
Copy link
Member

Bug Report

πŸ”Ž Search Terms

strictFunctionTypes inferred type parameter too narrow branded types

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________

⏯ Playground Link

Playground Link

πŸ’» Code

This is basically what I see in the TS repo.

export function tryCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined;
export function tryCast<T>(value: T, test: (value: T) => boolean): T | undefined;
export function tryCast<T>(value: T, test: (value: T) => boolean): T | undefined {
    return value !== undefined && test(value) ? value : undefined;
}

const enum SyntaxKind {
    ClassExpression,
    ClassStatement,
}

interface Node {
    kind: SyntaxKind;
}

interface Statement extends Node {
    _statementBrand: any;
}

interface ClassExpression extends Node {
    kind: SyntaxKind.ClassExpression;
}

interface ClassStatement extends Statement {
    kind: SyntaxKind.ClassStatement;
}

type ClassLike = ClassExpression | ClassStatement;

declare function isClassLike(node: Node): node is ClassLike;

declare const statement: Statement | undefined;

const maybeClassStatement = tryCast(statement, isClassLike);
const maybeClassStatement2 = tryCast<ClassLike, Node>(statement, isClassLike);

πŸ™ Actual behavior

The second type parameter to tryCast is inferred to be Statement, which is too narrow.

πŸ™‚ Expected behavior

The inferred type should be Node. This helper is used in the TS repo, but can't be used in some conditions because inference is wrong.

@ahejlsberg
Copy link
Member

This is working as intended. We infer the more specific Statement type for TIn because it satisfies both occurrences of TIn (the co-variant occurrence in the first parameter and the contra-variant occurrence in the second parameter), and we infer ClassLike for TOut based on the return type of isClassLike. TOut then fails its extends TIn constraint check because ClassLike doesn't extend Statement, so we default to the constraint, Statement. You need an extra type parameter to make this all work out:

declare function tryCast<TOut extends T, TIn extends T, T = any>(value: TIn | undefined, test: (value: T) => value is TOut): TOut | undefined;

// ...

const maybeClassStatement = tryCast(statement, isClassLike);  // infers tryCast<ClassLike, Statement, Node>(...)

@ahejlsberg ahejlsberg added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 16, 2022
@jakebailey
Copy link
Member Author

jakebailey commented Jul 16, 2022

Cool, I'll give it a try on my branch where I'm trying to enable this option.

@jakebailey
Copy link
Member Author

jakebailey commented Jul 16, 2022

Unfortunately that has a bad interaction with generic type guards.

For example, this also from the TS repo for our parenthesizer rules:

declare function cast<TOut extends T, TIn extends T, T = any>(value: TIn | undefined, test: (value: T) => value is TOut): TOut;

interface Node {
    kind: number;
}

interface TypeNode extends Node {
    typeInfo: string;
}

interface NodeArray<T extends Node> extends Array<T> {
    someProp: string;
}

declare function isNodeArray<T extends Node>(array: readonly T[]): array is NodeArray<T>;

declare const types: readonly TypeNode[];


const x = cast(types, isNodeArray); // bad: NodeAray<Node>


declare function oldCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut;

const y = oldCast(types, isNodeArray); // good: NodeArray<TypeNode>

Playground Link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants