6

When extracting Object.values from a Partial Record, the values are a union between what I would expect and undefined.

const example: Partial<Record<string, number>> = {}
const values = Object.values(example)

// strangely, the typing for values is as follows:
// const values: (number | undefined)[]

Instead of (number | undefined)[], I would expect it to be number[]. Every value in practice is defined, and, unless I'm missing something, always will be.

Code Sandbox showing the issue (with react scaffolding) can be found here

This seems likely to be some artifact of the type system, but I'd like to understand what's happening, and if there's any way to avoid this behavior.

Nathan
  • 73,987
  • 14
  • 40
  • 69
  • You made all properties optional with `Partial`, so `example = { a: undefined }` is valid which produces an `undefined` value. – Tobias S. Sep 13 '22 at 19:37
  • 1
    A type like `{ foo?: number }` allows for a value like `{}` or `{ foo: 42 }` or `{ foo: undefined }`. That's just how optional properties work. – VLAZ Sep 13 '22 at 19:37
  • I've been using `Partial object map, and I've been sort of thinking of like a Scala partial function, in that the range of the map is limited on the `string` type (not all strings result in an object in the map), as opposed to a Record that maps from an exhaustive Enum to some value. If I don't use partial, then indexing the record results in an object, but in reality it might be undefined. Is this a bad way of thinking about it? – Nathan Sep 13 '22 at 19:48
  • 1
    `Partial>` is equivalent to `{[x: string]: number | undefined}`, just like `Partial>` is equivalent to `{a?: number | undefined, b?: number | undefined}`. You might hope you could disable this `undefined`-adding behavior with `--exactOptionalPropertyTypes` but there's kind of a bug at [ms/TS#46969](https://github.com/microsoft/TypeScript/issues/46969) where `Partial` always adds `undefined` to index signatures even with that enabled. Would you like this written up as an answer? (If you reply, mention @jcalz or I won't be notified) – jcalz Sep 13 '22 at 19:52
  • An optional property simply means that `obj.foo` might give you `undefined`. Whether because `foo` doesn't exist or it exists and it's `undefined`. That's how you can think of optional properties. `Partial<>` makes all properties optional. – VLAZ Sep 13 '22 at 19:54
  • @jcalz An answer would be helpful if you have time, but I get what you're saying, I've been thinking of `Partial – Nathan Sep 13 '22 at 19:55
  • This blog describes the issue, but its solution is to use `Partial`: https://dev.to/sarioglu/avoiding-unintended-undefined-values-while-using-typescript-record-4igo – Nathan Sep 13 '22 at 19:57
  • 3
    Just use `{[k: string]: number}` aka `Record`. You don't need `Partial>`; there's no such thing as an "optional index signature", and index signatures already don't expect every possible property to be there (indeed, `{[k: string]: number}` would be a fairly useless type if you had to add a property for *every possible key*). You only need `Partial` for non-index signatures like `{a: number, b: number}`. Anyway I'll write up an answer when I get to it – jcalz Sep 13 '22 at 20:04
  • @jcalz but if `foo` is a `{[k: string]: number}`, and I access `foo["some-non-existant-key"]` it will be typed as `number`, not `number | undefined`. I'd like more type safety if possible-- see that blog I linked above, it goes more into the issue. – Nathan Sep 13 '22 at 20:52
  • Then you want `—noUncheckedIndexedAccess`, but it will annoy you in other situations. – jcalz Sep 13 '22 at 21:01
  • And if that’s important to you then you should put it in the question, otherwise it’s not obvious that it’s in scope. – jcalz Sep 13 '22 at 21:04
  • @jcalz ooh, that's what I was looking for, thanks! And fair enough, this kind of derailed from my original question. – Nathan Sep 13 '22 at 22:17
  • 1
    Okay I wrote up the whole crazy story of optional properties and index signatures and their sordid love/hate relationship with `undefined` – jcalz Sep 14 '22 at 02:07

1 Answers1

6

In everything that follows I will assume we are all using the --strict suite of compiler options, in particular the --strictNullChecks compiler option. If you don't have --strictNullChecks enabled then most of the following discussion involving the undefined type won't apply, but you probably wouldn't have the issue raised in this question in the first place.


The Partial<T> utility type is implemented like type Partial<T> = {[K in keyof T]?: T[K]} and applies the optional mapped type modifier to make all of the resulting properties optional.

The Record<K, V> utility type is a mapped type implemented like type Record<K, V> = {[P in K]: V}; and when you do that with string as the key it results in a string index signature like type Dictionary<V> = {[k: string]: V}; where any property whose key is of type string must have a value of type V.


I really wouldn't recommend mixing optional mapped type modifiers (Partial<T>) with index signatures (Record<string, V>). Both of them do, uh, interesting things with the undefined type by default, and there are compiler options to change this default behavior to produce other, uh, interesting behvior which some people prefer and other people do not. And no matter what settings you use, the combination of both of these together is probably going to make you unhappy, at least as of TypeScript 4.8.


First, optional properties: in --strict mode, TypeScript doesn't really distinguish missing properties from ones that are present-but-undefined. It's often reasonable to treat those as the same thing, since reading either give you undefined. But should you be allowed to write undefined to the value? Or should you be able to read undefined after you've used the in operator to check for the presence of the property? This depends on what you think "optional" means. By default, TypeScript says yes, if a property is optional then you should be able to write undefined there. And therefore {x?: string} and {x?: string | undefined} are identical types.

Don't like that? Well, neither did a lot of people... microsoft/TypeScript#13195, "Distinguish missing and undefined", was open for a long time and received hundreds of upvotes. And so with TypeScript 4.4 the --exactOptionalPropertyTypes compiler option was introduced. If you enable that, you will no longer be able to write undefined to an optional property unless undefined is explicitly included in the property type. Hooray!

But it's not part of the --strict suite of compiler options. Maybe if this feature had been introduced with --strictNullChecks in TypeScript 2.0 it would have been. But a lot of real-world code currently expects that optional properties accept undefined, and it would break if this were introduced. And it could be annoying. The simple copying of a property from one object to another of the same type, like a.x = b.x, suddenly isn't acceptable if the property is optional. You need if ("x" in b) {a.x = b.x} else {delete a.x} now. That's an extra hoop to jump through and if you don't really care the missing/undefined distinction then you're not going to be happy about it.

So that's optional properties.


Now for index signatures: in --strict mode, TypeScript doesn't really distinguish missing properties from ones that are present. This really doesn't matter for writing, but for reading it's noticeable. The compiler presumes that if you are reading from a key that matches the index signature, then you know a value is actually there. It doesn't account for the possibility that there is no property value and that you will get undefined. If you wanted to make safer reads you could manually add | undefined to the type yourself, turning {[x: string]: number} into {[x: string]: number | undefined}, but now you are trading the unsafe-read problem for an undesirable-write problem much like the one with optional properties. By default, TypeScript says that if you read from an index signature you're going to get a defined value (dereference at your own risk).

Don't like that? Well, neither did a lot of people... microsoft/TypeScript#13778, "Option to include undefined in index signatures", was open for a long time and received hundreds of upvotes. And so with TypeScript 4.1 the --noUncheckedIndexedAccess compiler option was introduced. If you enable that, you will now receive a possibly-undefined value when you read from an index signature but you can't write one there. Hooray!

But it's not part of the --strict suite of compiler options. Maybe if this feature had been introduced with --strictNullChecks in TypeScript 2.0 it would have been. But a lot of real-world code currently expects that index signature properties are present, and it would break if this were introduced. And it could be annoying. The simple iteration through an array by index, like for (let i = 0; i < arr.length; i++) { sum += arr[i]*arr[i] }, suddenly isn't acceptable. You need for (let i = 0; i < arr.length; i++) { const v = arr[i]; if (v !== undefined) { sum += v * v } } now. That's an extra hoop to jump through and if you don't use index signatures with arbitrary keys then you're not going to be happy about it.

So that's index signatures.


And you are combining them. Well, even with --exactOptionalPropertyTypes and --noUncheckedIndexedAccess enabled, Partial<Record<string, number>> is going to explicitly include undefined and therefore let you write undefined to its property values:

// @exactOptionalPropertyTypes: true
// @noUncheckedIndexedAccess
type X = Partial<Record<string, number>>;
// type X = { [x: string]: number | undefined; }

const x: X = {};
x.hello = undefined; // no error!

This is... a bug, I guess? Or a design limitation? It's filed at microsoft/TypeScript#46969 "exactOptionalPropertyTypes: Partial of index signature adds undefined" and marked as "Needs Investigation" as of TypeScript 4.8.

So no matter what you're looking for, you probably don't want to use Partial<{[k: string]: XXX}> at all.

Instead, you should think about what behavior you want given the available options above and their tradeoffs. If you are planning to only iterate over the properties of your object, then I'd say you should just use Record<string, number> (leave off Partial) and leave the default --strict options. If you are planning to index into your object's properties with arbitrary keys, then you should decide between the default settings and adding | undefined manually (accepting that undefined values may show up when you do iterate) or enable --noUncheckedIndexedAccess and cause every index signature in your code base to start getting really worried about undefined. It's up to you.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    A crazy, sordid story indeed! Thank you so much, this is incredibly informative. I've been using ` – Nathan Sep 14 '22 at 03:28