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

Unknown types narrowed to a union won't narrow further #30557

Closed
Validark opened this issue Mar 23, 2019 · 8 comments · Fixed by #38311
Closed

Unknown types narrowed to a union won't narrow further #30557

Validark opened this issue Mar 23, 2019 · 8 comments · Fixed by #38311
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@Validark
Copy link

Validark commented Mar 23, 2019

This fails on the playground, and I believe it also fails on [email protected]

TypeScript Version: [email protected]

Search Terms: unknown intersection failure types narrow

Code

interface TypeA {
	Name: "TypeA";
	Value1: "Cool stuff!";
}

interface TypeB {
	Name: "TypeB";
	Value2: 0;
}

type Type = TypeA | TypeB;

declare function isType(x: unknown): x is Type;

function WorksProperly(data: Type) {
	if (data.Name === "TypeA") {
		// data: TypeA
		const value1 = data.Value1;
	}
}

function DoesNotWork(data: unknown) {
	if (isType(data)) {
		if (data.Name === "TypeA") {
			// data: Type
			// data should be TypeA
			const value1 = data.Value1; // type error!
		}
	}
}

Expected behavior: in DoesNotWork, data should be TypeA after we can guarentee it is named "TypeA"

Actual behavior: data is still only a Type

Playground Link: Here

Related Issues: I looked through several pages of issues and did not find anything similar.

@Validark Validark changed the title Unknown types won' Unknown types won't narrow properly Mar 23, 2019
@Validark Validark changed the title Unknown types won't narrow properly Unknown types narrowed to an intersection won't narrow further Mar 23, 2019
@jack-williams
Copy link
Collaborator

jack-williams commented Mar 24, 2019

Narrowing by discriminant only applies when the declared type of the identifier is a union type; here, the declared type of data is unknown.

There are probably good reasons for this that someone more knowledgeable can provide, performance possibly among them.

There is an easy workaround which is declaring a new identifier inside the body of the first condition, then narrowing from that.

@Validark
Copy link
Author

Personally, I wish this was the way unknown worked out of the box because it seems to me that cases like the one I am describing would be extremely common for unknown types. The point of unknown is that you have to verify what type it is before using it. If you can't use the type normally after checking its type, that is a major problem in my view.

#27706 might be related.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Mar 24, 2019

interestingly this works:

function WorksProperly(data: Type) {
	if (data.Name === "TypeA") {
		// data: TypeA
		const value1 = data.Value1;
	}
}

function DoesNotWork(data: unknown) {
	if (isType(data)) {

		WorksProperly(data); // <-- !!!

		// if (data.Name === "TypeA") {
		// 	// data: Type
		// 	// data should be TypeA
		// 	const value1 = data.Value1;
		// }
	}
}

@jack-williams
Copy link
Collaborator

jack-williams commented Mar 24, 2019

I’m pretty certain the linked issue is not related. The behaviour as I describe is consistent with @Aleksey-Bykov ‘s example. The declared argument type of DoesNotWork is not a union, while the declared argument type of WorksProperly is.

While it is nice to have full reasoning applied to every situation, TypeScript gains a lot of performance wins by judiciously picking when and where to apply expensive checks.

I think this might be one of those cases, but I’m not certain. Someone on the team will be able to correct me.

I’ll add that I do agree that your example would be nice to have, and iteratively narrowing unknown is a good use case. The trade-off is whether you would special case unknown or apply the checks uniformly to all types.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Mar 24, 2019

what i've been told one day here is that narrowing doesn't make new types it only narrows what's given, in case of unknown it is implied all the types that only can be imagined out there (over 9000), which is well...
so... in the case of narrowing from unknown the design team clearly took a shortcut, and here we see the consequences

@RyanCavanaugh
Copy link
Member

This feels like it might be a bug. Once data is narrowed to a union type, narrowing by the discriminant is entirely reasonable

@jack-williams
Copy link
Collaborator

Fix here #30593

@talbenari1
Copy link

Out of curiosity, is this related? I didn't have to use unknown to lose type refinement.

type Foo = Bar | Baz;

interface Bar {
    kind: 'Bar';
    prop: string | void;
}


interface Baz {
    kind: 'Baz';
    prop: string;
}

function fn(el: Foo): number {
    switch (el.kind) {
        case 'Bar':
            if (!el.prop) {
                return 0;
            }
        // falls through
        case 'Baz':
            return el.prop.length; // Property 'length' does not exist on type 'string | void'
    }
}

@ahejlsberg ahejlsberg modified the milestones: Backlog, TypeScript 4.0 May 3, 2020
@ahejlsberg ahejlsberg added the Fix Available A PR has been opened for this issue label May 3, 2020
@Validark Validark changed the title Unknown types narrowed to an intersection won't narrow further Unknown types narrowed to a union won't narrow further May 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
6 participants