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

Using conditional typing to check for extends null does not seem to work correctly #29627

Closed
not-mike-smith opened this issue Jan 28, 2019 · 7 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@not-mike-smith
Copy link

TypeScript Version: 3.3.0-dev.201xxxxx

Search Terms:
Domain: Conditional Types
extends null

Code

type CannotBeNull<T> = {
    readonly [P in keyof T]: T[P] extends NonNullable<T[P]> ? true : false;
};

type DoesntCheckNull<T> = {
    readonly [P in keyof T]: T[P] extends null ? false : true;
};

type MyData = {
	name: string,
	age: number,
	hasChild: boolean,
	childAge: null | number,
	childName: null | string
};

type DataWhichArentNull = CannotBeNull<MyData>;
type DataAreAllTrue = DoesntCheckNull<MyData>;

Expected behavior:
The DoesntCheckNull<T> should have the same result as DataWhichArentNull<T> because it re-uses the logic in NonNullable<T>

i.e. DataWhichAreAllTrue should look like:

{
	name: true,
	age: true,
	hasChild: true,
	childAge: false,
	childName: false,
}

Actual behavior:
DataWhichAreAllTrue actually looks like:

{
	name: true,
	age: true,
	hasChild: true,
	childAge: true,
	childName: true,
}

Playground Link:
playground N.B. turn on --strictNullChecks flag in Options

Related Issues:
Possibly related to 25413.
I thought it was related to 23843, but that was fixed in a PR that was merged 19 days ago, but I could reproduce this issue even when using typescript@next.

@jack-williams
Copy link
Collaborator

You have flipped the branches in DoesntCheckNull, but you have not flipped the condition itself. Try:

type DoesntCheckNull<T> = {
    readonly [P in keyof T]: null extends T[P] ? false : true;
};

@not-mike-smith
Copy link
Author

@jack-williams, that works! I have no idea why. I don't understand why (null | T) doesn't extend null but is extended by null. Especially since the implementation of NonNullable<T> is

type NonNullable<T> = T extends null | undefined ? never : T;

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 28, 2019

For any types A and B, you can read A extends B as that I can always transform an A into a B.

The type null can be read as always having a null; the type null | T can be read as sometimes having a null, sometimes having a T.

Always having a null implies sometimes having a null which is why I can go from null to null | T. However, sometimes having a null does not imply I always have a null. I cannot always transform null | T into null because sometimes I might actually have a T instead.

Hope that makes sense!

@not-mike-smith
Copy link
Author

Yes, that makes sense, thank you! Seems like this issue can be closed as working as designed.

I'm still confused why the lib.es5.d.ts file has the T on the left side of extends instead of the right, but that is not really relevant

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 28, 2019

@smithmb-code

I'm still confused why the lib.es5.d.ts file has the T on the left side of extends instead of the right, but that is not really relevant

When you put the type parameter on the left hand side you get a distributive conditional type. This lets you do filtering on the type, so you can return the non-nullable parts. When you put the type parameter on the right hand side you reduce the conditional type to basically a yes/no answer.

type NonNullable<T> = T extends null  ? never : T;
type IsNullable<T> = null extends T ? T : never


NonNullable<number | string | null> // returns number | string, filtering null.
Nullable<number | string | null> // just returns the original type number | string | null

@not-mike-smith
Copy link
Author

Wow, that all makes sense now. Thanks again for explaining it! I see it explained in the docs now too.

@fatcerberus
Copy link

Don't feel too bad @smithmb-code, sometimes I make the mistake myself of thinking I can use extends to check for union membership (string | number extends number?) and then realize I can't do that because number | string is not actually a subtype of number--it's the other way around. Which is sometimes counterintuitive because you expect the thing with more terms to be more specific, but making a union in fact widens the type rather than narrowing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants