Skip to content

optional elements in objects cannot be ensured to be defined #39762

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
dcermak opened this issue Jul 27, 2020 · 9 comments
Closed

optional elements in objects cannot be ensured to be defined #39762

dcermak opened this issue Jul 27, 2020 · 9 comments
Labels
Duplicate An existing issue was already created

Comments

@dcermak
Copy link

dcermak commented Jul 27, 2020

TypeScript Version: 3.9.7, 4.0.0 beta, Nightly

Search Terms: required elements, type assertions

Code

interface Foo {
    element: number;
    optionalElement?: string;
}

// this one does not work, it fails to compile because optionalElement's type is incompatible
function toRequired(foo: Foo): Required<Foo> {
    if (foo.optionalElement === undefined) { throw new Error("element is undefined"); }

    return foo;
}

// this works
function toRequired2(foo: Foo): Required<Foo> {
    if (foo.optionalElement === undefined) { throw new Error("element is undefined"); }

    const { optionalElement, ...rest } = foo;
    return { optionalElement, ...rest };
}

Expected behavior:

It would be nice if toRequired would compile as well, as typescript knows that optionalElement is defined (as demonstrated in toRequired2.

Actual behavior:

toRequired fails to compile and requires either destructuring or type casts.

Playground Link:

Playground Link

@MartinJohns
Copy link
Contributor

It would be nice if toRequired would compile as well, as typescript knows that optionalElement is defined (as demonstrated in toRequired2.

It also knows that the object can still be mutated:

const foo: Foo = { element: 0, optionalElement: 'test' };
const requiredFoo: Reqired<Foo> = toRequired(foo);

foo.optionalElement = undefined;
requiredFoo.optionalElement; // Now undefined as well!

@dcermak
Copy link
Author

dcermak commented Jul 27, 2020

It would be nice if toRequired would compile as well, as typescript knows that optionalElement is defined (as demonstrated in toRequired2.

It also knows that the object can still be mutated:

const foo: Foo = { element: 0, optionalElement: 'test' };
const requiredFoo: Reqired<Foo> = toRequired(foo);

foo.optionalElement = undefined;
requiredFoo.optionalElement; // Now undefined as well!

That is a good point actually! But if optionalElement is declared as readonly, then toRequired still does not compile and the above snippet no longer works.

@MartinJohns
Copy link
Contributor

But if optionalElement is declared as readonly, then toRequired still does not compile and the above snippet no longer works.

I feel compelled to mention that readonly does not mean "immutable", and the property is only read-only via that interface. Interfaces with readonly properties are implicitly compatible with mutable interfaces.

interface ReadonlyData { readonly data: string; }
interface MutableData { data: string; }

function mutateData(obj: MutableData) { obj.data = 'example'; }

const data: ReadonlyData = { data: 'test' };
mutateData(data);

@dcermak
Copy link
Author

dcermak commented Jul 27, 2020

But if optionalElement is declared as readonly, then toRequired still does not compile and the above snippet no longer works.

I feel compelled to mention that readonly does not mean "immutable", and the property is only read-only via that interface. Interfaces with readonly properties are implicitly compatible with mutable interfaces.

I did not know that. Honestly, in that case I am actually not sure anymore if toRequired should even work the way I would like it to.

@jcalz
Copy link
Contributor

jcalz commented Jul 27, 2020

Duplicate of #16976, I think.

I don't see mutations as being the main issue here; control flow analysis could presumably narrow upon checking and then re-widen upon an assignment, much as it does in this case:

function toRequired(foo: string | undefined): string {
    if (foo === undefined) { throw new Error("element is undefined"); }
    return foo; // okay
}

function toRequiredBad(foo: string | undefined): string {
    if (foo === undefined) { throw new Error("element is undefined"); }
    foo = undefined; // I mutated it
    return foo; // error here
}

It's mostly that the compiler does not attempt to narrow the type of non-union-typed values, even if they contain properties that are themselves union types. I imagine performance would take a major hit if every check of a union-typed value had implications for any values depending on it.

Discriminated unions are possibly a way to proceed here, since they're one of the few places (or only place?) in which checking an object's property can narrow the type of the object itself. It still has to be a union-typed object, though, like this:

type Foo = ({ element: number }) & ({ optionalElement: string } | { optionalElement?: undefined });

function toRequired(foo: Foo): Required<Foo> {
    if (foo.optionalElement === undefined) { throw new Error("element is undefined"); }
    foo.optionalElement = undefined; // error, won't let you mutate
    return foo; // okay
}

@RyanCavanaugh
Copy link
Member

Duplicate #10065

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jul 29, 2020
@dcermak
Copy link
Author

dcermak commented Jul 30, 2020

Since #10065 was closed and locked, you're essentially saying that this won't be fixed/changed?

@RyanCavanaugh
Copy link
Member

Never say never, but it's unlikely to be any time soon. It would require a fairly invasive rethinking of how the checker reasons about types.

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants