TypeScript: Distinguish missing and undefined
TypeScript Version: 2.1.4
Code
Current, with --strictNullChecks on, the typescript compiler seems to treat
interface Foo1 {
bar?: string;
}
and
interface Foo2 {
bar?: string | undefined;
}
as being the same type - in that let foo: Foo1 = {bar: undefined}; is legal. However, this does not seem to be the original intent of the language - while some people have used ? as a shortcut for | undefined (e.g. https://github.com/Microsoft/TypeScript/issues/12793#issuecomment-266095767), it seems that the intent of the operator was to signify that the property either did not appear, or appeared and is of the specified type (as alluded in https://github.com/Microsoft/TypeScript/issues/12793#issuecomment-266092763) - which might not include undefined - and in particular it would let us write types that either have a property that, if enumerable is not undefined, or is not present on the object.
In other places it might make sense for ? to keep serving as a shorthand for | undefined, e.g. for function parameters. (or maybe for consistency it would make sense to stick with it meaning “always pass something undefined if you pass anything”? not sure what’s best there. Happy to discuss.)
Expected behavior:
interface Foo1 {
bar?: string;
}
function baz(bar: string) {};
let foo: Foo1 = {bar: undefined}; // type error: string? is incompatible with undefined
if (foo.hasOwnProperty("bar")) {
baz(foo.bar); // control flow analysis should figure out foo must have a bar attribute here that's not undefined
}
if ("bar" in foo) {
baz(foo.bar); // control flow analysis should figure out foo must have a bar attribute here that's not undefined
}
Actual behavior:
interface Foo1 {
bar?: string;
}
function baz(bar: string) {};
let foo: Foo1 = {bar: undefined}; // compiler is happy
if (foo.hasOwnProperty("bar")) {
baz(foo.bar); /* error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'*/
}
if ("bar" in foo) {
baz(foo.bar); /* error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'*/
}
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 338
- Comments: 62 (17 by maintainers)
Links to this issue
Commits related to this issue
- fix(unzip): fix Yauzl typings Both `open` and `openReadStream` have overloads, so `promisify` can't know which one to choose and chooses the last one (which doesn't include options). Additionally, o... — committed to ionic-team/native-run by tlancina 5 years ago
- fix Yauzl typings (#12) Both `open` and `openReadStream` have overloads, so `promisify` can't know which one to choose and chooses the last one (which doesn't include options). Additionally, once ... — committed to ionic-team/native-run by tlancina 5 years ago
- revert strict views, cannot express requirements due to limitations in TS See <https://github.com/Microsoft/TypeScript/issues/13195> — committed to qbs-nt/DefinitelyTyped by sbusch 4 years ago
- Revert strict views (with their disfunctional tests), but retain ViewProps for optional use (guarded with a test) Strict views cannot expressed du to current limitaion in TypeScript, see <https://git... — committed to qbs-nt/DefinitelyTyped by sbusch 4 years ago
- 🤖 Merge PR #46226 react-big-calendar: improve types, fix withDragAndDrop, fix tests by @sbusch * fix types for other components (compare with event and eventWrapper, they just have no special props)... — committed to DefinitelyTyped/DefinitelyTyped by sbusch 4 years ago
- 🤖 Merge PR #46226 react-big-calendar: improve types, fix withDragAndDrop, fix tests by @sbusch * fix types for other components (compare with event and eventWrapper, they just have no special props)... — committed to chivesrs/DefinitelyTyped by sbusch 4 years ago
- 🤖 Merge PR #46226 react-big-calendar: improve types, fix withDragAndDrop, fix tests by @sbusch * fix types for other components (compare with event and eventWrapper, they just have no special props)... — committed to danielrearden/DefinitelyTyped by sbusch 4 years ago
This feature is now implemented in #43947.
I agree, in JS there is a difference between the two
Is it possible to introduce
voidto mean “missing” and leaveundefinedas undefined? So{ foo?: string }would be equivalent to{ foo: string | void }?Update: separate issue with reduced test case https://github.com/microsoft/TypeScript/issues/35983
Another example where TypeScript’s inability to distinguish missing from
undefinedleads to inconsistencies between the types and JavaScript’s actual behaviour is usage of object spread.A real world example of this pattern is a
withDefaultshigher-order React component.Ideally, this would throw a type error:
On the other hand, if TypeScript did purposely not distinguish between missing and undefined, TypeScript should instead emit an error when spreading:
we have over 3500+ files project and never run into this problem, however you put it, typescript has enough expression power to please you, not sure why it draws so much attention
I have met JavaScript libraries that used
'prop' in optionsoroptions.hasOwnProperty('prop')to check for the presence of an option, and then when you passundefined(because you just pass your own optional parameter for example) it results in bugs TypeScript didn’t catch.My main issue is with the unsoundness of the spread syntax:
Another real world example where this matters.
React elements have their
classNameprop marked as optional. However, this also allowsundefinedto be passed as a value, resulting in HTML ofclass="undefined":Update: I was wrong https://github.com/DefinitelyTyped/DefinitelyTyped/pull/57463#pullrequestreview-820042607
I didn’t expect this feature coming any time soon, especially since it’s not even on the roadmap, and then BAM! Anders suddenly drops a feature-bomb on us… again! ❤️
The current missing/undefined convolution does seem potentially problematic…
This issue also manifests in
Object.values()andObject.entries()Playground
It would be nice to be able to safely describe an object that may be defined at a set of keys, but will never be defined as the value
undefined.Edit: For reference to an actual use case here, we have some Redux structures that cache items-by-id. So we need a record-like structure indexed on an arbitrary id. If we want to list all of the items in this cache, we end up using
Object.values().Started here: #30796
That’s a strong argument. It’s good for TypeScript’s type system to be able to express whatever is possible in JS, whether or not those possibilities are a good idea.
This keeps coming up from time to time. We’d like to see a PR that implements this so we can roughly estimate its complexity and see what the breakage on Real World Code is. It shouldn’t be a super-difficult PR to sketch out the basics for.
Basic possible outcomes:
--strictwith an opt-outWhat about a
‑‑noImplicitUndefinedcompiler flag that would make optional parameters and properties not automatically allowundefined.Any updates on this one? Does seem to be unfortunate that
permits 3 possible runtime states to handle.
Closed #38624 in favor of this one. I’ll add a note on this issue.
Here’s yet another example that leads to a runtime error:
I expected the program to fail to compile because any prop in
Partial<A>can be explicitly set toundefined, which overrides it’s corresponding value when spreaded over another object of typeA. This causes the return value offnto not be described byA, but instead by an interface similar toAwhere all props can beundefinedas well. But got:TypeError: "aRes.a is undefined"instead.Playground Link: https://www.typescriptlang.org/play?ts=4.0.0-dev.20200516#code/JYOwLgpgTgZghgYwgAgELIN4Chm+QIwC5kQBXAW32iwF8stRJZEUBBTHPOY1AblvoIA9iADOYZDBDIAvMgAUcKAHNiABSVhgcADYAeVgD4AlMXYzDCjNwxEATDQA0yAHRulymsf5YdECXAAShCispIg8taEpCAAJhAwoBCxXvzCYkJ+LjpCyorBoi5wLvjeQA
Hmm, in #24897 (tuples in rest/spread positions):
That makes me wonder if we might use
T?(whereTis a type) everywhere to mean “an optional parameter/property whose type, if not missing, isT”. So we allow the?to jump from the parameter/key name to the type. That is, the following function signatures are nearly equivalent:(modulo whether you want one or both of them to disallow
foo(undefined)) and the following types are nearly equivalent:(modulo whether you want one or both of them to disallow
{a: undefined}). And themissingtype would be equivalent tonever?.I find myself wanting this feature more now, since #26063 (mapped arrays/tuples), as away to programmatically add/remove optionality from tuples.
I would like to see this ability added. I commonly use an object as a dictionary, mapping strings to values.
In this case you are not sure which keys are in the object, but you know that if they are in the object they have a value of type
T(i.e., notundefined). So what I want is this:Currently TypeScript does not flag the first increment as an error. I can understand that: it is necessary for TypeScript to assume you are using a valid key; otherwise using arrays would be tiresome. However, it would be nice if I could use the following type to mean that some keys may be missing, not to mean that some keys may have an undefined value.
This currently does flag the first increment as an error, as desired. However, it also flags the second and third increments as errors since it assumes their values may be undefined. If TypeScript distinguished between missing keys and keys with an undefined value then I believe this second type would do what I want.
@robbiespeed well following ES6 semantics that should really be made possible with just
because according to ES6, passing
undefinedto a parameter is equivalent to passing no parameter - if you declared a default value for example, passingundefinedwill result in the default value being used, which would not be true for e.g. passingnull.I scanned through all of the above comments to see if somebody else had already mentioned this use case regarding index signatures, but I couldn’t find anything, so here goes.
Update: the above error no longer reproduces in 4.2 due to this change: https://devblogs.microsoft.com/typescript/announcing-typescript-4-2-rc/#relaxed-rules-between-optional-properties-and-string-index-signatures (https://github.com/microsoft/TypeScript/pull/41921).
and
Here is the same issue reported on StackOverflow: https://stackoverflow.com/questions/64505407/coercing-type-with-optional-properties-to-an-indexable-type
If TypeScript distinguished between missing and
undefinedthen I believe this would not produce an error. That is because the optional property would not addundefinedto the property value type.Real world use case: I’m trying to convert an object with an optional property into a
Jsontype.Workarounds I’m aware of:
JsonRecord, addundefinedto the index signature However this feels like we’re creating another problem because now the JSON type is incorrect—JSON objects cannot containundefinedvalues, but this type will allow objects which containundefinedvalues.JsonRecord, replace the index signature with a mapped type and mark all properties as optional: This is probably better than the previous workaround because we’re declaring the property as optional. However, this will still allow objects which containundefinedvalues, due to the fact that TypeScript does not distinguish between missing andundefined. Example of a library using this workaround: https://github.com/sindresorhus/type-fest/pull/65I’ve hacked this by using tag-type 😃 May be this snippet will help anybody. Seems working for my case …
converting this type
into this type
snippet with example
Playground Link
@jcalz I know 100% soundness isn’t a goal but this made me chuckle
Playground
Because
voiddoesn’t mean “missing” orundefined. It means “a value exists but you should not use it” or something to that effect.Sharing my workaround:
I’m not sure I really agree that generics are an issue. IMO it just seems weird to ask a value if it is missing by consulting its type; by definition you have a value so it can’t be missing! The only thing that knows whether there should be a value there or not is the container, be it an object or function. Once you have a value, of possibly generic type, the concept of missing has already been lost. The semantics of the container determine how to interpret missing.
I don’t think is a problem that is ever going to be solved by adding a special type - it really needs to be pushed up a level to be a property of fields and arguments.
voidis broken for a variety of reasons; I hope TypeScript 4.0 can readdress this now thatvoidis largely redundant withunknown.Note that
voidmeaning “missing” has already been implemented for function parameters (with no trailing required parameters) in #27522:So that’s closer to this being a real thing. Of course there are still some outstanding bugs here (#29131); and it doesn’t yet work on object properties; and we can still call
foo("a", undefined)so the part where this distinguishes “missing” fromundefinedisn’t there. But it’s a start, right?For anyone still waiting for optional function arguments, it is now possible to simulate that using new tuple types and spread expressions.
it has a limitation with inferred argument type though, which is solved by explicitly specifying undefined argument type
Unfortunately the optional tuples suffer from the same problem:
I do think their is mileage in the syntax
T?though I think it should just be a type with a meaning that is invariant of context.T?should say nothing about properties or arguments and be valid anywhere a type is. I think thatT?is allowed in JsDoc types, though I’m not sure how this plays with TS.Some my thinking would be something like: in
strictMissingModethen{ x?: string }means either the property is not present, or the property is explicitly present and the value is of typestring. The?is an attribute of the property, not the typestring. Herestringmeans exactly what it says.{ x: string? }means that the property is explicitly present but the value is either of typestringorundefined. The?is an attribute of the type sostring?means the same thing in all contexts. Essentiallystring? === (string | undefined).The same would apply to function parameters, with the following behaviour.
So the existing type
{ x?: string }in vanilla TS would be equivalent to{ x?: string? }under the strict mode, which it sort of gets expanded out to anyway. In the strict mode,{ x?: T }and{ x: T? }coincide only when the value has the propertyxof typeT.Additionally I think the strict mode should do away with the unsound optional property weakening rule that says:
{ ... }is assignable to{ ...; y?: string }if the former doesn’t have a conflictingyproperty. Instead the rules should be:{ x: T }is assignable to{ x?: T }.{ ... }is assignable to{ ...; y?: unknown }.The reverse can be applied through
innarrowing such that:a : { x?: T }then"x" in anarrows to{ x: T }.a : objectthen"x" in anarrows to{ x?: unknown }, or some suitable intersection type.The strict mode could also have some interesting interactions with
never. Right now a type{ x: never }is sort-of meant to mean thatxdoes not exist, though the type should really be isomorphic tonever. The type{ x?: never }says either the property does not exist, or it exists with typenever, so only the former can be true. The type{ [K in string | number | symbol]?: never }might represent the true empty object.yeah… if I was designing javascript from scratch, I’d probably leave out the whole concept of
undefined. But it exists in js, and I don’t think typescript is willing to try to change that, so we have to live with it. Making it possible to distinguish, in the type system, between an attribute existing or not without making that attribute optionally undefined would be useful, since we are working with a language that has these (perhaps unnecessary) complexities.Sorry for a new not-so-relevant post but I just wanted to reply to last @fdc-viktor-luft comment.
The specific case you presented is better handled by
Pickinstead ofPartial. I would made a signature more like this:Now TS will fail if you pass
undefinedto a prop that doesn’t allow it explicitly. See playground. BTW, this is howsetStateis typed in React.That being said I really believe that differentiate “missing” from
undefinedis necessary. Wish to see some progress on this.@Vanuan
never | TisT. I get what you are trying to do, but unfortunatelyneveris not meant to model this situation.The crux of the problem, unfortunately, is that the type system can’t model this today; not that the syntax for it is overly verbose. @masonk hit the nail on the head in https://github.com/microsoft/TypeScript/issues/13195#issuecomment-269620662
part of the problem here is that typescript’s interfaces are open. For instance (using a
missingtype to indicate optional key/value pairs, without implyingundefinedis a legal value):If we try to model
Foo1as{bar: string} | {}we have the problem that{}is short for "an interface with no fields, … or with arbitrary keys and values.As for a path forward, I see two options: a) try to tackle this in combination with some sort of “closed” concept for interfaces, or for single keys on interfaces b) try to tackle this similarly to the “object literals don’t have excess properties” checks, which avoids the need for working this thoroughly into the algebra of the type system
in (a) we would want something like
An alternate version, using a special
missingtype for everything instead ofBanKeys<>that I made upI’m less convinced by the general usefulness of option (b). There, we would get similar safety as option (a), but only on object literals passed directly to the libraries using
missingin their type annotations. In practice I’d expect this would sometimes be useful, but probably not sufficient for most cases.FYI a small extra example when it is needed.
It is a necessity when you are working with Firestore. Firestore doesn’t allow you to store
undefinedvalues and responds with error. Currently TS doesn’t catch an explicitundefinedvalues you accidentally pass to Firestore. You usually can do it intentionally or using spread syntax on object.For this particular case I have to handle that via autotests (I can mock firestore client and track that we never pass an explicit
undefinedvalue in tests). But that is not ideal (it is difficult to make a full tests coverage) and I would prefer TS to track that.If I’m not mistaken, the importance of the distinction is most important for mapped types, e.g. for a
patchObject()function.But having
?coverundefinedas is currently the case is also useful, e.g. for monomorphic object factories.The former pattern is probably more common than code that requires making the distinction between
missingandundefined. It is important because it lets JS engines optimize objects with a hidden class, whereas adding or deleting keys triggers slow code paths.So the current semantics give nice ergonomics to a pattern that’s useful in practice.
So it would be nice to let users have it both ways. The
| voidsuggestion above would be a possibility, but the presence of a key is part of the type of the object, and that syntax makes it look like it’s part of the type of the value. Hence my proposal in #26438 to introduce a?!suffix that would introduce the semantics proposed here, and keep?as it is now.This would also be backwards compatible without the need of a “codemod” script to update every optional property.
@shmidt-i My understanding is that if you declare a variable of type
Record<string, number>, then whatever property you will take from it, it should return anumber. Your typeUncertainShapeis explicitly against that assumption that’s why it throws an error.Here you can see a similar example. What is in my opinion misleading, is that second line is OK. I personally would prefer to get error in both situations.
It would be useful now to be able to define whether a property should be on an object or not based on some conditional type.
You can use
undefinedcurrently but as discussed it’s not quite the same.@tom-sherman You can do it if you constrain
TtoArray<any>since function arguments are a tuple that extends Array. See example below. You can probably also do it without the constraint by using conditional types or function overloads. It is a trade-off between cosmetic beauty and simplicity of the implementation.Yeah, I wanted this:
let o: { field: never } = { };. Apparently, this is invalid code. So there’s no way to model a type of an object which is guaranteed to not have a given key or no keys at all.That is, there’s no difference between plain objects and duck-like objects.
Which means TypeScript is still inherently a duck-typed language.
So, the fix should include:
never | Tinstead ofnever | undefined.undefinedto optional. That isundefinedshould not be assigned tonever | TifTdoesn’t includeundefineddeleteshould only be allowed for fields of type that includesneverand some other type. It should not be allowed todeletea field of typeundefined.field: never | numbershould becomefield: numberafterdelete a.fieldoperation. But if you’re passing the reference to an object, of course its type shouldn’t change, because it doesn’t change in runtime.It’s debatable what should happen when assigning an optional field to a new variable. Should the result type be
never | Tornever | T | undefined. We can’t really know which is it. If the field is optional and was not present it would beundefinedin runtime. But this behavior should be an error usage of a field. So maybe it’s a warning? E.g. “please use type guard to ensure the property is present in the object”. Well, most functions don’t supportnevertype as an argument, so maybe it doesn’t matter. You would need a type guard anyway.@AnyhowStep that’s a really good point, it has two conflicting meanings right now. In
f: We don’t care about the value, it could be anything Infoo: The value is missing and evaluates as falsePlayground
@jcalz this seems like a really bad bug, especially if people are using
voidto mean missing.What’s worse is there doesn’t seem to be a way to fix this without breaking backwards compatibility
@elektronik2k5 I would not recommend to do this particular thing on backend. Sometimes thrown error is better than saving [potentially] invalid data. Some value can be set to
undefinedaccidentally (e.g. instead ofnull/0/""/falseor hundreds of other reasons).That’s why we love TS - once you covered your code with a good types you don’t need to take care of such things (write an extra programmatic layer for validation and/or autotests).
Probably(!) Firestore library contributors will be the first who apply this TS feature in their codebase. And I’m quite sure they intentionally do not suppress
undefinedvalues on their library side before saving.Typescript version 3.3
strictNullChecksenabled:not sure how we arrived at talking about arguments, but an arguments-less example:
IMO that’s less ugly that using
argumentsdirectly, but it’ll transpile to arguments if your target is es5.But anyhow this topic was originally about missing keys vs present keys with an
undefinedvalue within an object. That feels like a different case than missing vs. present arguments to functions - though if we can figure out a way to unify the two concepts in the type system I’m all for it.@felixfbecker this is not how it currently works, Typescript will complain stating
Expected 1 arguments, but got 0.Also
foo()andfoo(undefined)are not exactly equivalent. Take the following example:For arrow functions the semantics would be the same though since
argumentsis not accessible.I’m on the fence about whether Typescript should function according to the exact semantics or not. It’s a bit of an edge case, and as far as I know use of
argumentsis not something Typescript makes an effort to account for anyway.Where does this issue currently stand? Is it something under consideration, or are there reasons why we can’t fix this?