Skip to content
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

Type inference fails to recognize never when it's the result of a template tag function #61039

Open
FUDCo opened this issue Jan 24, 2025 · 4 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@FUDCo
Copy link

FUDCo commented Jan 24, 2025

πŸ”Ž Search Terms

tagged template literal never type

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about the never type and about template literals.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABMAhjANgFQKYFsAO6KU2AFAM5QBOMYA5uQFyI4FEkDK1tDAglVRQBPADSIAdJIBuKdCGxNEKMEIDaAXQCUzMNinYqiAN4AoROcRQAFlTgB3RLocBRAXCqkA5KgytCxbCV0O2FySxt7ck9NAG4TAF8TE1BIWARkNHQAMXBoeDBSbUc9A2MzC2tbBydEV1sPb0yc1PygkKEwysjouMSTAHp+xABhdypsaHQhRHGIODowGAAvBXDAlLz0iFl0RAATOFWwOCgZ7CgQKjBk3LSkKCF8bGGrCYBrA1GBCagpgBkYB9yJgrDByKQUFQ6AB+ZiUGj0IrwnhlCyIGDARAQqGaVFoizjC5XJRQuJoxJonzZW75Qq9JKDRAASUgYx+U2QRAYazORPuj0Cdhg1mYzU2SCIEDeYWwYD2KMJlyQlACuFlp2Ue15SssAv2hzCx1OtAgcj2gU84HNwFo2D2nnEpAATABmABsbs0Nxa6QeTxe7wMLLm30mQgAIgaAHInAEfEFg7EwuHcREphF0PHmDFYyF0XGmfEE846vNkiwUixUvzsbAAAwARig9nX6QMhl9Zr9puRsKtrMQeb25nLtcSVSQ1WBjWFwOMUBArCgG+hsN7xbr-a8pZ82WHIwoY1A49gE+C87DEMi01fU5nCxYc0mC+Ui4riWXX4hK+YqWK7nSX7vkgnivOg6BwJ4baMlkmSdHAV59uEg7WIEw4IFqwG3qq6rorOYDzouy6ruudybs824fFQwZ7t2J7AqC55Qpe175umKIPtmmLPlm+JYZ+5JftWeD+CQjbNq2QElsSoHYOBkH0kAA

πŸ’» Code

function failTemplate(strings: TemplateStringsArray, ...values: any[]): never {
    throw new Error('failTemplate always throws');
}

function failFunction(): never {
    throw new Error('failFunction always throws');
}

// Correctly recognizes the function call does not return
function typeCheckerCorrectlyLikesThis(arg?: string): string {
    if (arg) {
        return arg;
    }
    failFunction();
}

// Incorrectly flags the return type with: Function lacks ending return statement and return type does not include 'undefined'.(2366)
function typeCheckerIncorrectlyDoesNotLikeThis(arg?: string): string {
    if (arg) {
        return arg;
    }
    failTemplate`bad`;
}

// Correctly sees that the second return statement is unreachable
function typeCheckerCorrectlyDoesNotLikeThis(arg?: string): string {
    if (arg) {
        return arg;
    }
    failFunction();
    return 'hello';
}

// Fails to see that the second return statement is unreachable
function typeCheckerIncorrectlyLikesThis(arg?: string): string {
    if (arg) {
        return arg;
    }
    failTemplate`bad`;
    return 'hello';
}

πŸ™ Actual behavior

The invocation of a template tag function (via a tagged template literal) that is declared to return type never terminates the flow of control of the containing function, but the type checker does not recognize this and complains as if execution had continued beyond the invocation.

It appears, to my naive sensibilities, that the use of a tagged template literal is not recognized as a function invocation even though it is one. However, if the return type of the tag function is, say, number, the type inference engine does correctly recognize this, so it seems like the issue is specifically with the flow control logic associated with never.

πŸ™‚ Expected behavior

A template literal using a never returning template tag function should be recognized the same as an ordinary invocation of a never returning function.

Additional information about the issue

No response

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jan 24, 2025
@RyanCavanaugh
Copy link
Member

Only analyzing call statement is an intentional trade-off to achieve reasonable performance (since making new control flow nodes incurs expense on all analysis); you'd see the same thing if you had written e.g.

function typeCheckerIncorrectlyDoesNotLikeThis(arg?: string): string {
    if (arg) {
        return arg;
    }
    const p = failTemplate`bad`;
}

In situations like this, you can write

return failTemplate`bad`;

which will be analyzed as requested

@FUDCo
Copy link
Author

FUDCo commented Jan 24, 2025

I would argue that a template invocation is a call statement (which it is in terms of everything but syntax), so I don't see any fundamental semantic difference between the case that's handled correctly and the case that's handled wrong in terms of the control flow analyses they imply. If I write the code in the form that is handled correctly I'm paying the performance cost of that analysis, and that case is much more common so the performance savings of skipping it in the other case seems like a de minimis gain. Especially considering that this is a build-time performance cost. The whole point of using a tool like TypeScript is to spend cycles at build time to avoid having to spend cycles at run time.

More to the point, the very reason many of us use TypeScript is because we are concerned about correctness. Choosing to sacrifice correctness for (build-time!) performance seems absolutely bizarre to me. I can make anything at all infinitely fast if I don't care if it's correct.

Also, and though I'll grant that this is not dispositive, the return workaround you suggest runs afoul of some common lint rules (as does the comparable alternative of using throw there).

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Jan 24, 2025
@RyanCavanaugh
Copy link
Member

Fair points. Can I ask why you chose to use a template tag here instead of a function call?

@FUDCo
Copy link
Author

FUDCo commented Jan 24, 2025

Certainly!

The use case is for constructing assert-like functionality. Traditionally, asserts are written something like:

assert(condition, `the ${foo} is not compatible with the ${bar}`);

where the condition being false results in the second argument being used as the message in an Error that gets thrown. However, if you're writing highly paranoid code that wants to have lots and lots of these, and you'd also like the descriptions of the resulting error conditions to be highly detailed and informative, BUT the asserts detect actual problems only very rarely (which is what you want), then (assuming you are writing in a language like JavaScript where assert is not a special form) you can end up wasting an awful lot of CPU cycles generating strings and evaluating template literals that are then immediately thrown away because the condition was fine (this is not speculative, this is an actual performance problem that I and my cohorts have suffered in a number of production systems).

However, you can instead use a pattern like this:

condition || Fail`the ${foo} is not compatible with the ${bar}`;

where the tagged template Fail unconditionally throws an Error with the given error message string. In this case, boolean short circuit evaluation means that the Fail template literal is never evaluated in the (normal) case where the condition is true.

You could, of course, just write:

if (!condition) {
    throw new Error(`the ${foo} is not compatible with the ${bar}`);
}

but notationally this is much more verbose and clunky, which detracts considerably from readability if you have a lot of them and also discourages developers from using these kinds of checks as liberally as we'd like.

A plausible fallback might indeed be to replace the tagged template with a function call:

condition || Fail(`the ${foo} is not compatible with the ${bar}`);

Arguably this raises the question why one should have to insert a couple of nugatory parentheses just to have the flow of control analyzed right, but that's academic because TS doesn't correctly analyze this case either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants