Blind Despair
Blind Despair

Reputation: 3295

Why doesn't typescript undefined type behave same as optional?

Imagine we have interface

interface Foo {
  bar: number | undefined;
}

If we try to create object of type Foo like

const foo: Foo = {};

It won't compile because property bar is missing. But we say that it can be undefined, which will work if we explicitly set it to undefined, but that's exactly same if we do not set it at all. Shouldn't it do exactly same as following?

interface Foo {
   bar?: number;
}

For me this is an issue, because if we consider more complex example, where we have interface with a field, which can be optional by generic type. So like, if generic type is not specified, then field should be undefined, if it is specified, then it should be only of that type. For example

interface Foo<T = undefined> {
    bar: T;
    title: string;
}

const foo1: Foo = {
    title: 'TITLE'
};

const foo2: Foo<number> = {
    title: 'title',
    bar: 12
};

foo1 will fail to compile because property is missing, but it anyway has to be undefined, and if we specify it explicitly it will work, but that's exactly same. I ended up solving this problem with inheritance, where base class doesn't have any generic parameters and the child has it strictly specified. But I am just curious if anyone knows a specific reason why undefined type is handled this way. Because I couldn't find any information about it myself.

Upvotes: 24

Views: 10676

Answers (6)

Albert Ko
Albert Ko

Reputation: 11

You could possibly use the PartialOnUndefinedDeep type in type-fest to help with this https://github.com/sindresorhus/type-fest/blob/main/source/partial-on-undefined-deep.d.ts

For example:

import { PartialOnUndefinedDeep } from "type-fest"

type X = {
  a: number | undefined;
  b: number;
}

function takesInX(input: X) {  
  return input.b + (input.a ?? 0);
}

// will fail type check
// takesInX({b: 5});

function takesInXFixed(input: PartialOnUndefinedDeep<X>) {  
  return input.b + (input.a ?? 0);
}

Upvotes: 0

jfMR
jfMR

Reputation: 24738

TypeScript modeling of optional properties

TypeScript models JavaScript run-time behavior when it comes to reading an optional property:

interface Foo {
  bar?: number;
}

type Bar = Foo['bar'];

The Bar type is number | undefinedX. This makes sense since the bar property may be missing, and reading a missing property evaluates to undefined.

When it comes to setting an optional property, however, TypeScript doesn't model JavaScript run-time behavior properly (at least, not by default):

const foo: Foo = {
  bar: undefined,
};

The code above type checks even though undefined is not explicitly stated as a constituent of bar's type. Unfortunately, this doesn't work nicely with JavaScript's in operator:

"bar" in foo; // --> true

So, if we assign undefined to the bar property – which does not explicitly include undefined as part of its type – the property is not considered missing.

Enabling exactOptionalPropertyTypes for proper modeling of optional properties

The option exactOptionalPropertyTypes was introduced to address the issue exposed above. With exactOptionalPropertyTypes enabled, the following code generates a type error:

const foo: Foo = {
  bar: undefined,
};

Giving us a hint:

Consider adding 'undefined' to the types of the target's properties.

Now, for bar to accept undefined, its type must explicitly include undefined:

interface Foo {
  bar?: number | undefined;
}

XTechnically, it's only true provided strictNullChecks is enabled, which is enabled by default if strict is. Otherwise, Bar would be number, and JavaScript run-time behavior would not be modeled appropriately when reading optional properties.

Upvotes: 0

Luke Pring
Luke Pring

Reputation: 992

Typescript 4.6

type TNullProperties<T> = {
  [K in keyof T as null extends T[K] ? K : never]?: T[K];
};

type TNotNullProperties<T> = {
  [K in keyof T as null extends T[K] ? never : K]: T[K];
};

Theses two can be used together to make only the null properties of an object optional.

{ key1: string | null; key2: string }

becomes

{ key1?: string | null | undefined; key2: string }

Upvotes: 1

Almaju
Almaju

Reputation: 1383

A solution that worked for was to use this utility type:

type KeysOfType<T, SelectedType> = {
  [key in keyof T]: SelectedType extends T[key] ? key : never;
}[keyof T];

type Optional<T> = Partial<Pick<T, KeysOfType<T, undefined>>>;

type Required<T> = Omit<T, KeysOfType<T, undefined>>;

export type OptionalUndefined<T> = Optional<T> & Required<T>;

Example

type MyType = {
    foo: string | undefined;
}

const willFail: MyType = {};

const willNotFail: OptionalUndefined<MyType> = {};

Taken from https://github.com/Microsoft/TypeScript/issues/12400#issuecomment-758523767

Upvotes: 3

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249556

The simple reason appears to be that nobody has implemented it yet. The way the current type checker is implemented it ended up requiring properties of type undefined to be required, but there is a proposal to change the behavior in a way that makes more sense, but nobody has gotten to it yet.

A workaround that keeps the same name for the type and makes the field optional could be achieved using conditional types:

type Foo<T = undefined> = {
    title: string;
} & (T extends undefined ? {} : { bar: T});

const foo1: Foo = {
    title: 'TITLE'
};

const foo2: Foo<number> = {
    title: 'title',
    bar:10
};

Upvotes: 11

Joe Clay
Joe Clay

Reputation: 35797

The two type signatures aren't entirely equivalent (although they're close enough that the difference may not be apparent at first glance)!

  • bar?: number expresses that the object might not have a field called bar.
  • bar: number | undefined expresses that the object will always have a field called bar, but the value of that field might be set to undefined.

This difference might matter in some cases, as some runtime behaviors are dependent on the difference between a field being present and a field being set to undefined - consider if you called Object.keys on the object:

Object.keys({ bar: undefined }) // returns ["bar"]
Object.keys({})                 // returns []

Upvotes: 38

Related Questions