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)

Playground Link: https://www.typescriptlang.org/play/?ssl=2&ssc=22&pln=1&pc=1#code/MYewdgzgLgBADgJxHAXDArmA1mEB3MGAXhgG8YsBTATxjXIDcBDAG3UrQCIAJSllkDADqIBCwAmnGAF8ZAbgBQC0JFgALPgLyiJxeEjgB+AHRVqJ5m0pygA

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 27
  • Comments: 24 (1 by maintainers)

Most upvoted comments

I think it should be possible to do optional chaining on unknown.

@Barbiero 4?.foo is not an error in javascript:

$ node
> 4?.something
undefined

This is exactly the point of ?..

you would expect type inference to just fall through the unknown.

IOW:

declare var x: unknown;
const y = x?.anything;
// expect type of y to be unknown

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:

const myflag = args?.group?.flag;
if (typeof myflag === 'string') {
  // use myflag
} else {
  // report usage error
}

Same for JSON.parse (which should output unknown, not any, if it had been introduced today):

function typeSafeJSONParse(s: string): unknown {
    return JSON.parse(s);
}

function main(cfg: string): void {
    const o = typeSafeJSONParse(cfg);
    if (o?.mygroup?.myflag) {
        doMyThing();
    }
}

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 unknown does

As a type, unknown means that the type is well, unknown. When you declare an object as unknown, 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 prop has a key, which has a value:

const helloworld = (prop as {key?:{value:string} } | undefined)?.key?.value
// helloworld is now string | undefined

that’s intentional design for unknown to force the developer to reason about the value’s typing before using it. You could use any, but I feel you’d be shooting yourself on the foot there since any simply 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 of Partial<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:

function isTokenObject(obj: any): obj is TokenObject {
  return (
    typeof obj?.signature === "string" &&
    typeof obj?.header?.alg === "string" &&
    (typeof obj?.payload === "object" && obj?.payload?.exp
      ? typeof obj.payload.exp === "number"
      : true)
  );
}

These functions are used to validate types and I use optional chaining operators.

If I would change the parameter types to unknown I would have to evaluate much more expressions because I could not use the optional chaining operator.

As a type, unknown means that the type is well, unknown. When you declare an object as unknown, Typescript gives up any type inference whatsoever and decides that the type must be cast before the variable can used.

@Barbiero then why can I use the typeof operator on an unknown type? To verify the type maybe? Right, this is exactly what the optional chaining operator is for, too.

Unknown can be any type, including non-objects. Consider

const prop: unknown = 4

prop?.something // error!

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 is number, is a runtime error. unknown protects 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 use unknown - 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 prop has not the type of {prop: {something: 3}}, for example. unknown should act with regards to the optional chaining operator exactly like the any type does:

const prop: any = 4
prop?.something // no error!

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 any type although this should be exactly a case for unknown. Like you said with regards to unknown before:

the type must be cast before the variable can used

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 unknown is 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 prop of type unknown. We know nothing of its type.

If we write prop?.key, there are three scenarios.

  1. prop has a key property. Our expression resolves to the value of that property.
  2. prop does not have a key property (because it’s {} or 4). our expression resolves to undefined.
  3. prop is nullish. Had we written prop.key, then a TypeError would have been thrown[spec]. But since we’re using ?., our expression also resolves to undefined[spec].

Although we know nothing about the type, we know we can safely write prop?.key.

The counterargument

Imagine we have a variable named tree of type undefined | { branches: Array<Branch> }.

tree?.branches is OK. However, TS prevents us from writing tree?.tentacles.

If we were to decide that ?. is allowed on variables of type unknown, then tree?.tentacles would become OK once we change the type to unknown. 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 variable can be undefined.

const variable = undefined as unknown;
if (variable?.prop?.something) {
  // do stuff
}

Javascript equivalent works:

const variable = undefined;
if (variable?.prop?.something) {
  // do stuff
}

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 any you could do prop.key.value which 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 unknown type 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 on unknown. Further, the result of the optional chaining on unknown would be unknown | 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.iProbablyShouldNotDoThis and the compiler would accept it. However, by returning unknown it protects me from introducing a runtime error.

I feel it’s important to reconsider what @timonson brought up: using unknown instead of any for type guards… if optional chaining works with unknown, then we have safer code because the compiler will now require us to use optional chaining instead of just switching off type safety altogether with any.

If we were to decide that ?. is allowed on variables of type unknown, then tree?.tentacles would become OK once we change the type to unknown. In other words: knowing less about the type would allow us to do more. That doesn’t seem right either.

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 unknown before making the next assertion. Knowing less allows us to do more…

As a type, unknown means that the type is well, unknown. When you declare an object as unknown, Typescript gives up any type inference whatsoever and decides that the type must be cast before the variable can used.

@Barbiero why can we apply the typeof operator to an unknown type? Right, to verify the type. This is the function of the optional chaining operator, too.

or when you want to enforce runtime type checking.

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 unkonwn in catch clauses lands in the next version. I bet someone will open a similar issue. @typescript-eslint/no-implicit-any-catch brought me here.

What’s really annoying is that it works perfectly fine at runtime. helloworld will be initialized with “Hello World”

The counterargument

Imagine we have a variable named tree of type undefined | { branches: Array<Branch> }.

tree?.branches is OK. However, TS prevents us from writing tree?.tentacles.

If we were to decide that ?. is allowed on variables of type unknown, then tree?.tentacles would become OK once we change the type to unknown. In other words: knowing less about the type would allow us to do more. That doesn’t seem right either.

This can be solved by not autocompleting tentacles property 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 Partial type. Maybe it could be really handy to add more power to Partial and make all properties and nested ones optional, so that consumer code must handle undefined cases.

Luckily it is easy to define such behaviour in TS with a recursive type:

type RecursivePartial<T> = {
    [P in keyof T]?: RecursivePartial<T[P]>;
}

(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 unknown looks like a good idea, specially with a really flexible type system such as typescript.

If we were to decide that ?. is allowed on variables of type unknown, then tree?.tentacles would become OK once we change the type to unknown. In other words: knowing less about the type would allow us to do more. That doesn’t seem right either.

(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.