TypeScript: optional chaining does not work for unknown
Search Terms: optional chaining unknown
Code In a real world scenario I have an api which returns unknown
const prop: unknown = { key : { value: "Hello World" } };
const helloworld = prop?.key?.value;
Expected behavior:
I want to assign prop?.key?.value to helloworld, since it exists;
Actual behavior: I get the following error for prop: const prop: unknown Object is of type ‘unknown’.(2571)
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 27
- Comments: 24 (1 by maintainers)
I think it should be possible to do optional chaining on
unknown.@Barbiero
4?.foois not an error in javascript:This is exactly the point of
?..you would expect type inference to just fall through the
unknown.IOW:
And since we’re all talking hypotheticals, maybe some use cases could help illustrate the point: I’m using this for command line option parsers. Akin to JSON.parse, they can output any object of any form. So you want to do:
Same for JSON.parse (which should output
unknown, notany, if it had been introduced today):as @nkappler mentioned, the reason to do this is that it helps avoid me accidentally using the output of JSON.parse (or arg parsing) without explicitly checking each independent key. This helps guard against runtime errors.
@RyanCavanaugh: voting to reopen
seems like you’re misunderstanding what
unknowndoesAs a type,
unknownmeans that the type is well, unknown. When you declare an object asunknown, Typescript gives up any type inference whatsoever and decides that the type must be cast before the variable can used.So you need to actually tell TS that
prophas akey, which has avalue:that’s intentional design for
unknownto force the developer to reason about the value’s typing before using it. You could useany, but I feel you’d be shooting yourself on the foot there sinceanysimply ignores typing altogether.I don’t understand why you are trying to use unknown on either of these cases. If you know what type it is(which means you know the type’s probable representation), then you don’t use unknown. Unknown is a top level type meant to represent an… unknown… Type. When you want to tell consumers of your functions that pretty much anything can come out of the function, or when you want to enforce runtime type checking.
If you know the type, then it’s not
unknown. Use your type accordingly. Make use ofPartial<T>or| undefined. Optional chaining will be enabled, as expected.When you declare something
unknown, then Typescript can’t infer it’s type - because you just said it is not known. If you want the type to be inferred, you don’t declare a type.You’re trying to use the wrong tool here.
@Barbiero it does NOT cause a runtime error.
Take a look at this module example, please: https://github.com/timonson/deno/blob/dc95ec020d90381412e6ff9145a3f9feda1074b0/std/jwt/validation.ts
I use various type predicate functions there. For example:
These functions are used to validate types and I use optional chaining operators.
If I would change the parameter types to
unknownI would have to evaluate much more expressions because I could not use the optional chaining operator.Unknown can be any type, including non-objects. Consider
Optional chaining won’t solve this.
@timonson how is your example “no error” when it will cause a runtime error?
The point of unknown is that you must type check before using it, because you don’t know what that type is.
prop?.something, when the underlying value isnumber, is a runtime error.unknownprotects you against that.Optional chaining isn’t even relevant here. Its purpose is to protect you against potentially null or undefined values. What you are asking is that optional chaining forgoes any type checking at all.
What you want is to use the type
any. And in that case, you need to pay attention to possible runtime errors. If you want both a top level type and compile-time error checking, then you useunknown- in which case, you must either type cast or type check(with a guard, for example).@Barbiero this is exactly what we are complaining about. It should not be an error, because in your example it would help to verify that
prophas not the type of{prop: {something: 3}}, for example.unknownshould act with regards to the optional chaining operator exactly like theanytype does:I guess my main point is the following. To use one of JavaScript’s best operators to validate types I am forced to operate this operator on the
anytype although this should be exactly a case forunknown. Like you said with regards tounknownbefore:I would love to use optional chaining exactly for that. But I understand that the TypeScript team has to make compromises, of course.
@Barbiero mentioned that
unknownis designed to “force the developer to reason about the value’s typing before using it”. That is an excellent way of putting it. I think the point of many in this thread is: (?.) shows you’re doing exactly that ‒ reasoning about the value’s typing before using it.The argument
Take OP’s example. We have a variable named
propof typeunknown. We know nothing of its type.If we write
prop?.key, there are three scenarios.prophas akeyproperty. Our expression resolves to the value of that property.propdoes not have akeyproperty (because it’s{}or4). our expression resolves toundefined.propis nullish. Had we writtenprop.key, then aTypeErrorwould have been thrown[spec]. But since we’re using?., our expression also resolves toundefined[spec].Although we know nothing about the type, we know we can safely write
prop?.key.The counterargument
Imagine we have a variable named
treeof typeundefined | { branches: Array<Branch> }.tree?.branchesis OK. However, TS prevents us from writingtree?.tentacles.If we were to decide that
?.is allowed on variables of typeunknown, thentree?.tentacleswould become OK once we change the type tounknown. In other words: knowing less about the type would allow us to do more. That doesn’t seem right either.@Barbiero what prevents me from using optional chaining right on underlying value? I would expect the following to work since I am considering the fact that
variablecan be undefined.Javascript equivalent works:
And there’s no error: I just executed the snippets in TypeScript Playground if I change to
any(1) and Chrome console (2). You may have to read up on optional chaining.Well I do understand what unknown does, however I expected this to work, because writing a typeguard or casting the value feels cumbersome if you need the value only once. And since optional chaining is a save way to access the properties if they exist, in my opinion it should be possible as an easy way to say “gimme that if it’s there”, without the need of explicit typing. I understand that it is intentional though, otherwise it would behave too similar to any I guess…
also with
anyyou could doprop.key.valuewhich might not be safe and it would be a nice feature if you could do optional chaining without types but you would get an error without the questionmarks…Another way to look at it is that the
unknowntype is really all possible types in union. It’s the entirety of the type space. Since the entirety of the type space interacts safely with optional chaining, then optional chaining should be allowed onunknown. Further, the result of the optional chaining onunknownwould beunknown | undefined === unknown.I think mathematically it makes sense, and that’s a strong argument for it to be included in the type system. But I guess it depends on how we determine what’s in and what’s out. Is TypeScript’s main goal to catch run-time errors before they happen or is there a “style” of code that it’s trying to encourage as well? Although I appreciate languages that try to enforce idioms, I don’t really even see a value to such an idiom 🤔. And it’s been a frequent friction point since we’ve started to lint against explicit any.
And to be clear this does not mean that optional chaining forgoes type checking. If that were the case I could write
unknownVariable?.mightBeHere.iProbablyShouldNotDoThisand the compiler would accept it. However, by returningunknownit protects me from introducing a runtime error.I feel it’s important to reconsider what @timonson brought up: using
unknowninstead ofanyfor type guards… if optional chaining works withunknown, then we have safer code because the compiler will now require us to use optional chaining instead of just switching off type safety altogether withany.But that’s exactly what existing TypeScript documentations recommends. For example, if there is insufficient overlap between two types during an assertion, you’ll be told to cast it as
unknownbefore making the next assertion. Knowing less allows us to do more…@Barbiero why can we apply the
typeofoperator to anunknowntype? Right, to verify the type. This is the function of the optional chaining operator, too.This is the precise use of the optional chaining operator. The suggestion above to review how the operator works is clearly still applicable.
We did not get an explanation why is the current behaviour intended. No one disproved OP’s point. Let’s wait until the feature of typing errors as
unkonwnin catch clauses lands in the next version. I bet someone will open a similar issue.@typescript-eslint/no-implicit-any-catchbrought me here.What’s really annoying is that it works perfectly fine at runtime. helloworld will be initialized with “Hello World”
This can be solved by not autocompleting
tentaclesproperty and also by forcing the use of “indexed” property access (tree?.["tentacles"]) instead of “dot” property access when noPropertyAccessFromIndexSignature config option is on.The discussion is still alive in #37700. My hope is that this argument gets traction because the TypeScript documentation itself has relied on the set theoretic approach to explain some of its counter-intuitive behaviors e.g. a type union has the intersection of properties from the associated types. What’s good for the goose is good for the gander, so to speak.
Optional chaining is relatively new, and as more shops go the “no explicit any” route, this will get felt by the community as a pain point. That’s what drove me here lol.
Brilliant arguments but I’m afraid nobody is listening. What would it take to bring this to TS team attention again? a petition of some sort?
I was gonna comment in favour of OP, but after some thinking, I see @Barbiero point.
If you know the aproximate data structure, then just cast the any type returning function to some
Partialtype. Maybe it could be really handy to add more power toPartialand make all properties and nested ones optional, so that consumer code must handleundefinedcases.Luckily it is easy to define such behaviour in TS with a recursive type:
(from https://stackoverflow.com/questions/47914536/use-partial-in-nested-property-with-typescript/47914631)
I wonder how much torture is applied to the compiler using recursivity, but that’s another issue.
I would avoid type casting as the plague though. I don’t like the compiler trusting in me, as I am a human prone to errors, but that’s just my opinion.
Enforcing runtype checking with
unknownlooks like a good idea, specially with a really flexible type system such as typescript.(emphasis mine)
I’d call this expected behaviour: if you change the type to “unknown”, that is precisely what it is: unknown. You could make the same argument for
any, or any other typecast. Casting a value means that you explicitly know, for sure, that the new type is a better description of that value than the old. If that is not true, then it’s the cast that was wrong, not the consequences in the typesystem.