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

Indirect type narrowing via const #12184

Closed
gasi opened this issue Nov 11, 2016 · 19 comments · Fixed by #44730
Closed

Indirect type narrowing via const #12184

gasi opened this issue Nov 11, 2016 · 19 comments · Fixed by #44730
Assignees
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript

Comments

@gasi
Copy link

gasi commented Nov 11, 2016

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 ahejlsberg added the Suggestion An idea for TypeScript label Nov 13, 2016
@ahejlsberg
Copy link
Member

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
Copy link
Author

gasi commented Nov 14, 2016

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

@electricessence
Copy link

I believe. :|

@RyanCavanaugh RyanCavanaugh added the Revisit An issue worth coming back to label Mar 15, 2018
@shayded-exe
Copy link

Any progress on this? Really annoying to deal with.

@ronaldruzicka
Copy link

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
Copy link

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

const isPagination = typeof offset === 'number' && typeof limit === 'number';

@amannn
Copy link

amannn commented Oct 1, 2020

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

@maraisr
Copy link
Member

maraisr commented Nov 23, 2020

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
Copy link
Contributor

AlCalzone commented Dec 11, 2020

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
Copy link
Contributor

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

@monfera
Copy link

monfera commented Jan 21, 2021

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
Copy link

Nokel81 commented Jan 22, 2021

@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
Copy link
Member

RyanCavanaugh commented Jan 22, 2021

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 #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
Copy link

Nokel81 commented Jan 22, 2021

@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
Copy link

monfera commented Jan 22, 2021

@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
Copy link

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

@ahejlsberg
Copy link
Member

Now implemented in #44730.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.