Skip to content

strictNullChecks with optional properties on class change type #38323

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

Closed
hasezoey opened this issue May 4, 2020 · 11 comments
Closed

strictNullChecks with optional properties on class change type #38323

hasezoey opened this issue May 4, 2020 · 11 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@hasezoey
Copy link

hasezoey commented May 4, 2020

TypeScript Version: 4.0.0-dev.20200504

Search Terms: optional class strictNullChecks generics type

Code

type RefType = number | string | Buffer;
type test<T, S extends RefType = number> = T | S;

class SomeClass {
  public prop?: test<string>;
  // type of "prop" when "strictNullChecks" is disabled "test<string>" (how it should be)
  // type of "prop" when "strictNullChecks" is enabled "string | number | undefined"
}

Expected behavior:
that prop is of type test<string> (or in strict mode test<string> | undefined)

Actual behavior:
prop is of type string | number | undefined

Playground Link:
not working as expected: Playground Link

working as expected: Playground Link

Related Issues:


the current behavior breaks functions that depend on destructuring types

function getRefProperty<T, S extends RefType>(ref: test<T, S>): S {
  // some code
}
@nmain
Copy link

nmain commented May 5, 2020

test<string> is exactly equivalent to string | number, and test<string> | undefined is exactly equivalent to string | number | undefined, so there's nothing wrong there.

The way you've written getRefProperty you'll never get useful inferences for S. Inference proceeds left to right starting at T, and the sole argument to the function is a T | ..., so T will be inferred to simply be the type of the argument in all cases. But then S is entirely unconstrained by the parameter values, so there's nothing to do but set it to some sensible default and hope things work out.

Keep in mind that types are structural and type unioning is commutative and associative, so whenever you have T | S it doesn't matter to typescript what "part" of the type is in T and what "part" is in S.

getRefProperty is a type assertion in disguise.

// returns "chicken noodles", of course
declare var c: SomeClass;
getRefProperty<string | number | undefined, "chicken noodles">(c.prop);

@andreialecu
Copy link

andreialecu commented May 5, 2020

@nmain take a look at this example:

class ObjectId {
  anObjectId!: boolean;
}

export declare type RefType = number | string | ObjectId | Buffer;

type Ref<
  R,
  T extends RefType = R extends { _id: RefType } ? R["_id"] : ObjectId
> = R | T;

declare function isRefType<T, S extends RefType>(
  doc: Ref<T, S>,
): doc is S;


class Model {
  _id!: string; // change to ? and error below disappears
  name!: string;
}

class Parent {
  child!: Ref<Model>;
} 

const x = new Parent();

isRefType(x.child) ? x.child.anObjectId : x.child._id;

Here's a Playground Link

Depending on whether _id is defined as nullable or required, the type of Child changes from Ref<Model, ObjectId> to Ref<Model, string>, and a compilation error appears below.

I assume there's a type error somewhere in this code, but the issue originated from a similar example.

@andreialecu
Copy link

andreialecu commented May 5, 2020

Thinking about it, it seems that if a property is defined as nullable, then the extends check is false. Is there any way to work around this?

_id: string | undefined is ok, but id?: string is not

@hasezoey
Copy link
Author

hasezoey commented May 5, 2020

@nmain then why does it work (as expected by me) when strictNullCheckes is false?
(so that the type is test<string, number>(test<string, number> | undefined) and not string | number | undefined)

The way you've written getRefProperty you'll never get useful inferences for S

when strictNullCheckes is false it is fully working as it is written, and it is very useful, that is why i made an issue on why it changes with this option

-> the whole intention about getRefProperty is to automatically infer the types needed

@nmain
Copy link

nmain commented May 5, 2020

then why does it work (as expected by me) when strictNullCheckes is false?

If the argument's type is test<A, B> for some A and B and that hasn't been expanded or simplified or modified at all yet, then tsc sees that it's trying to fit test<T, S> and does the obvious thing and infers T as A and S as B.

@hasezoey
Copy link
Author

hasezoey commented May 5, 2020

@nmain yes, but shouldnt when strictNullCheckes (that adds | undefined) is active result in test<A, B> | undefined where it still could resolve the function?

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label May 8, 2020
@RyanCavanaugh
Copy link
Member

I don't see anything indicative of a bug here. Please file a new issue with a more explicit explanation of what's wrong if you'd like us to come back to it - "This isn't what I expected" is not really indicative of a defect without more context.

@andreialecu
Copy link

@RyanCavanaugh here's an example where behavior is different with strictNullChecks on or off:

Playground Link

Just toggle strictNullChecks on or off.

The curious part is that simply changing the type on the first line to add undefined to it:

export type ObjectId = { _id: string } | undefined;

Resolves the issue, but it's unclear why. So I don't think there's a typescript bug here, just non-intuitive behavior.

@andreialecu
Copy link

Nevermind, I understand the problem. More context here if someone runs into this in the future and needs help: typegoose/typegoose#254 (comment) :)

Issue can probably be closed.

@hasezoey
Copy link
Author

hasezoey commented May 9, 2020

thanks @andreialecu for explaining why this is happening, but i would have expected an error from typescript of "incompatible types" or something like that

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

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