microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.77k stars 12.46k forks source link

Indirect type narrowing via `const` #12184

Closed gasi closed 3 years ago

gasi commented 7 years ago

TypeScript Version: 2.0.3

Code

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

type Shape = Square | Rectangle;

function area(s: Shape) {

    // // Doesn’t compile: Using a `const` as a type guard:
    // // `error TS2339: Property 'size' does not exist on type 'Shape'.`

    // const isSquare = s.kind === 'square';
    // if (isSquare) {
    //     return s.size * s.size;
    // }

    // // `s` is not narrowed to type `Rectangle`

    // Compiles: Directly using `s` in the type guard:
    if (s.kind === 'square') {
        return s.size * s.size; // `s` has type `Square`
    }

    // `s` is narrowed to type `Rectangle`

    return s.width * s.height;
}

Expected behavior:

s’s type can be narrowed indirectly via a const.

Actual behavior:

s’s type can only be narrowed via direct access.

ahejlsberg commented 7 years ago

This would require us to track what effects a particular value for one variable implies for other variables, which would add a good deal of complexity (and associated performance penalty) to the control flow analyzer. Still, we'll keep it as a suggestion.

gasi commented 7 years ago

Thanks, @ahejlsberg, I appreciate the feedback and understand the complexities.

electricessence commented 7 years ago

I believe. :|

shayded-exe commented 5 years ago

Any progress on this? Really annoying to deal with.

akomm commented 4 years ago

@ahejlsberg I am comming from this issue being closed as duplicate of this. It was about destructuring tuples and narrowing down the type.

While the reason for the issue is the same, the use-case is a different one. But since the other has been marked as duplicate of this I have to ask it here:

Would it make it simpler if that would only be allowed, when destructuring to const variables AND only affecting the correlation between the destructured values. This would probably not allow the use-case of @gasi but the one I linked?

Use case example would be something like this:

type ResultA<TOk, TErr> = [TOk, null] | [null, TErr];

function divide(a: number, b: number): ResultA<number, Error> {
  if (0 === b) {
    return [null, new Error('Division by zero')];
  }
  return [a / b, null];
}

const [okA, errA] = divide(10, 5);
const res = divide(10, 5);

if (!errA) {
  const plusOneA = okA + 1; // does not work
}

if (!res[1]) {
  const plusOneB = res[0] + 1; // works
}
kamilic commented 4 years ago

Any progress on this?

ronaldruzicka commented 4 years ago

How is this issue looking right now? I think I have a similar problem and every other issue was linking to this one.

If I have a condition the types are working correctly, but when I extract it into variable I get a TS error: Object is possibly 'undefined'.

type Config = {
    limit?: number;
    offset?: number;
}

const showPagination = ({limit, offset}: Config) => {
    const isPagination = limit && limit > 0 && offset && offset >= 0

    // Doesn't work?!
    if (isPagination) {
        return limit * offset;
    }

    // Works!
    if (limit && limit > 0 && offset && offset >= 0) {
        return limit * offset;
    }

    return null
}

Or check this playground: https://tinyurl.com/y7nrawkh

ronaldruzicka commented 4 years ago

to even simplify you can change it to typeof check and still not working properly.

const isPagination = typeof offset === 'number' && typeof limit === 'number';
amannn commented 4 years ago

Yep, I think that's this particular issue. I raised a similar one which was marked as a duplicate of this issue.

maraisr commented 3 years ago

~How are we looking at a resolution on this?~ it's been 4 years, is this going to be a reality at some point? Or should we treat this as an anti-pattern, and simply not use it?

AlCalzone commented 3 years ago

I just stumbled across this and I'm wondering what puzzle pieces are missing to support this at least for single variables. Given that there is now the possibility to have custom type guards (functions with a return type of s is Square) for example:

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

type Shape = Square | Rectangle;

function isSquare(s: Shape): s is Square {
    return s.kind === 'square';
}

function area(s: Shape) {
    if (isSquare(s)) { // works
         return s.size * s.size;
    } else {
        return s.width * s.height;
    }
}

couldn't it be possible to use this special type for variables too? For example like this:

// ... other definitions like above
function area(s: Shape) {
    const isSquare: s is Square = s.kind === 'square';
    if (isSquare) {
         return s.size * s.size;
    } else {
        return s.width * s.height;
    }
}

Based on my limited understanding of the TypeScript internals, this example doesn't seem fundamentally different from the case where a type guard function is called. Maybe it would even be possible to infer the type s is Square when the right hand side tests a property of a discriminated union.

ExE-Boss commented 3 years ago

It would more likely infer: s is Extract<typeof s, { readonly kind: "square" }> or s is (typeof s) & { kind: "square" }.

monfera commented 3 years ago

I agree with @ronaldruzicka that the typeof is an even simpler case.

I agree with the proposal from @AlCalzone as a stop gap because it retains almost all the brevity of the original ask by @gasi and may address a good part of the objection by @ahejlsberg that it'd require flow analysis; it doesn't appear structurally different to the function case at least when the const is directly used (substitutability).

I also believe that for TypeScript to live up to its promise, it needs to bite the bullet and do flow analysis. It's at a point of maturity, being worked on since 2012. Many closely related tools including TS tools themselves already engage in various ways of flow analysis.

TypeScript maintaining this gap promotes poor coding practices, because one can't extract and name (sometimes common) constants, which is one of the most basic tools for readable, maintainable and efficient code.

The decision process around TypeScript evolution comes into question, based on this issue and other issues where TS is also a leaky, non-contiguous abstraction, eg. the claim is structural typing, yet extra properties of objects most often fly under the radar (except with literals, an unexpected counterexception within the exception), despite it impacts structure just as much as missing properties do.

TypeScript has leapt ahead in many esoteric, rarely used directions, while the basic quilt has gaps. One of the biggest is, lack of nominal typing, one can add angles in radians with angles in degrees because number.

Prompted by this issue, I kindly ask for an executive revision of TypeScript toward the effect of enumerating and prioritizing issues, most of them with a history of so many years, so that its adoption is not hampered by such behaviors that are counterintuitive and/or

TypeScript has grown a lot, it'd be useful to take a breath, look back and marvel at the amazing progress, and finally complete the basics for a tight, homogeneous baseline strength, while also reducing many of the pain points in the learning curve of TS, many of them caused by incidental rather than essential complexity, or suboptimal prioritization of solving gaps, an example for which is this one, four years and counting.

TS has more potential to fill than this.

Nokel81 commented 3 years ago

@AlCalzone I (personally think) that having val is T as a "type" that extends Boolean is the best solution for this. That way would could even do things like using bind and have the return types be "correct"

RyanCavanaugh commented 3 years ago

The decision process around TypeScript evolution comes into question, based on this issue and other issues where TS is also a leaky, non-contiguous abstraction, eg. the claim is structural typing, yet extra properties of objects most often fly under the radar (except with literals, an unexpected counterexception within the exception), despite it impacts structure just as much as missing properties do.

Prompted by this issue, I kindly ask for an executive revision of TypeScript toward the effect of enumerating and prioritizing issues, most of them with a history of so many years, so that its adoption is not hampered by such behaviors that are counterintuitive

This is a very confused interpretation of history and the current state of reality. TypeScript has always had structural subtyping, but detecting excess properties in object literals was a very-frequently logged issue before that behavior existed (see https://github.com/microsoft/TypeScript/issues/391 and its many incoming duplicates). This feature was implemented to much positive feedback at the time, and the behavior prior to excess property checking was widely cited as one of TS's worst shortcomings.

To say that we're not paying attention to unexpected behavior by citing one of the most-requested changes we ever had (as measured by the proportion of feedback from users at the time) is a paradox.

Nokel81 commented 3 years ago

@RyanCavanaugh (I know that I just came in on this discussion and was mostly directed here from #24865). Do you see a future of having user type predicates having more then just one level of propagation?

monfera commented 3 years ago

@RyanCavanaugh thanks for your reply,

[strict property checking of object literals] feature was implemented to much positive feedback at the time

Sorry for being unclear, I agree this behavior is the expected thing, also a step toward completing structural typing, worth extending it for non-literals too. There'd be a way to signal intent for additional keys, maybe named after Common Lisp's &allow-other-keys. There can be a permissive legacy mode just like suppressExcessPropertyErrors was.

Yet, literals excess props check fragmented the behavior into two parts. This split behavior, which makes TS a tad more complicated, was my only reason for citing it.

In the current case, the split is between inline use vs. binding to a variable name.

TS thus weakens referential transparency; the extracted out way has no type assurance while inline use does. Steepened learning curve; hard to read code; denied refactoring options.

Evolving TS must be incredibly hard, given all constraints, starting with JavaScript. Having said this, orthogonality is key to learning and using a language. It contrasts with the accreting, discursive, historical, accidental path of eg. philosophy, or ambiguities, special cases and reinvention in biology.

Improving data flow analysis is in general, hard, but escape analysis, eg. by lexical scoping and argument passing, TS can prove local places of the use, and a const a: boolean is immutable; the odd parameter reassignment can be checked, done even by eslint while editing. Language users rarely ask for data flow analysis directly; but it's the common tool for what folks wish to have, even just ticket has a big stream of duplicates.

For example, the issue you linked refers to the value of catching typos. While it's good for lax JS to acquire an overlay of assurance so that it don't happen, and had to be a popular ask, it's rare for a programming language to be driven by the goal of catching typos. Ideally their concepts are fewer, more versatile and rooted, and resiliency against typos just follows from the deeper aspects.

To stay with the current issue at hand, it feels there's consensus about the utility of the issue ask and @AlCalzone's suggestion, as there's no objection other than, it's hard to implement. This is also but one example where common, proven coding patterns could be better supported by TS, and in the absence of such support, undesirable compromises are made, that reduce code quality, sometimes from many aspects eg. type safety, runtime, developer experience, and inclusivity toward new developers.

When TS started, it had immediate use by the most sophisticated JS developers, lessened the need for property checks in unit tests: most users were expert in JS and stack.

As now TS is very popular, it's good to instead think of it as a language, and from the viewpoint of new developers from a diverse background who learn TS as their first programming language.

Where can one read up on TS stewardship discussions on what larger goals and values drive the evolution of TS, and what milestones are foreseen? Hard to make out a language just by ingesting a stream of our imperfect userland asks on gh.

The issue ask was apparently not foreseen at the initial release of TS; it's concerning to see the length of time since asking for a widespread language property, followed by silence with no progress, plan or even problem acknowledgement linked to this issue, despite how heavily we developers rely on, and take for granted, referential transparency, eg. substitutability of an expression with its shorthand, when shaping our code.

AuthorProxy commented 3 years ago

so will this be fixed? I tired to duplicate typeof x === "string" at every if

ahejlsberg commented 3 years ago

Now implemented in #44730.