Skip to content

Optional chaining of unknown should be unknown #37700

Closed

Description

@hktonylee

Search Terms

Optional chaining, unknown

Suggestion

Currently it is forbidden to use optional chaining on unknown. But for all possible types in JavaScript, optional chaining will return undefined if it does not apply or cannot find the field. So we should safely regard optional chaining of unknown type to be unknown as well.

This is the verification of optional chaining on all possible types in JavaScript:

const num = 5;
const str = '6';
const undef = undefined;
const nul = null;
const bool = true;
const sym = Symbol('sym');
const obj = {};
const func = () => {};

console.log(num?.field);    // print undefined
console.log(str?.field);    // print undefined
console.log(undef?.field);  // print undefined
console.log(nul?.field);    // print undefined
console.log(bool?.field);   // print undefined
console.log(sym?.field);    // print undefined
console.log(obj?.field);    // print undefined
console.log(func?.field);   // print undefined

Use Cases

This is to simplify type checking on nested object with unknown type. For example, we should be able to do something like this:

function getFieldFromJson(json: unknown): undefined | number {
  const field = json?.fieldA?.fieldB?.fieldC; // <--- this
  if (typeof field === 'number') {
    return field;
  }
}

Currently we will need to cast explicitly on someUnknown, someUnknown.fieldA, someUnknown.fieldA.fieldB and someUnknown.fieldA.fieldB.fieldC. People will find this is unnecessary verbose and may just fallback to use any.

Examples

As above

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Activity

  1. changed the title [-]Optional chaining of unknown should be allowed[/-] [+]Optional chaining of unknown should be unknown[/+] on Mar 31, 2020
  2. ilogico commented on Mar 31, 2020

    @ilogico

    This is as safe as allowing property access on {} (unknown is pretty much the same as {} | null | undefined) or allowing access to unknown properties in interfaces in general.
    I don't think you should be able to do something with a type and not with its sub-types (we already have any for these things).

    And if this would be allowed, why should it be restricted to the optional chaining syntax?

    function f(value: unknown): unknown {
      if (value != null) return value.someProp;
      return undefined;
    }

    I believe it is semantically equivalent.

  3. RyanCavanaugh commented on Mar 31, 2020

    @RyanCavanaugh
    Member

    I think the real weirdness is that it'd break the operational directionality of unknown. Normally for any type S that's a subtype of T, if an operation is allowed on T then it's definitely allowed on S, but now unk?.prop is valid when e.g. ({ x: 1 })?.prop isn't even though { x: 1 } is by definition a subtype of unknown.

    This would lead to some subtle breakage in the future, e.g. let's say you wrote

    function fn(x: unknown) {
      if ('y' in x) {
        console.log(x?.z);
      }
    }

    The proposal is to make this code OK, but then in the future it'd be a breaking change for us to narrow x to { y: unknown } in the body of the if, which seems bad - narrowing should only ever increase the set of things you're allowed to do.

  4. falsandtru commented on Mar 31, 2020

    @falsandtru
    Contributor

    Should use x?.['y'] instead.

  5. hktonylee commented on Apr 1, 2020

    @hktonylee
    Author

    I think the real weirdness is that it'd break the operational directionality of unknown. Normally for any type S that's a subtype of T, if an operation is allowed on T then it's definitely allowed on S, but now unk?.prop is valid when e.g. ({ x: 1 })?.prop isn't even though { x: 1 } is by definition a subtype of unknown.

    This would lead to some subtle breakage in the future, e.g. let's say you wrote

    function fn(x: unknown) {
      if ('y' in x) {
        console.log(x?.z);
      }
    }

    The proposal is to make this code OK, but then in the future it'd be a breaking change for us to narrow x to { y: unknown } in the body of the if, which seems bad - narrowing should only ever increase the set of things you're allowed to do.

    Hi Ryan thank you for your excellent example. I agree that narrowing should only ever increase the set of things that are allowed to do. But I can think of a counterexample involving the use of contravariance.

    interface Action {
        actionType: string;
    }
    
    interface KeyboardAction {
        actionType: 'keyboard';
        key: string;
    }
    
    let genericHandler: (payload: Action) => void;
    
    function isAction(a: unknown): a is Action { return true; }
    
    function isKeyboardAction(a: unknown): a is KeyboardAction { return true; }
    
    function test(unknownAction: unknown) {
        // This will report error for subclass
        if (isKeyboardAction(unknownAction)) {
            type Action = typeof unknownAction;
            const handler: (action: Action) => void = {} as any;
            genericHandler = handler; // <-- Error here
        }
    
        // Same code as above, but this will *NOT* report error for superclass
        if (isAction(unknownAction)) {
            type Action = typeof unknownAction;
            const handler: (action: Action) => void = {} as any;
            genericHandler = handler;
        }
    }
    

    As you see class narrowing does not always increase the set of actions that are allowed to do. I believe this ?. operator also faces similar situation.

    Beside theoretical issue, in practice use, people will just be fine to fix the optional chaining of a variable when they decide to narrow the type. Comparing to using any, this feature will indeed benefit more than the harms.

    Regarding the breaking change concern, if this proposal is published along with the type narrowing of unknown of in operator, we should be free from introducing breaking change.

  6. omeid commented on Dec 17, 2020

    @omeid
    function f(value: unknown): unknown {
      if (value != null) return value.someProp;
      return undefined;
    }

    I think the real weirdness is that it'd break the operational directionality of unknown. Normally for any type S that's a subtype of T, if an operation is allowed on T then it's definitely allowed on S, but now unk?.prop is valid when e.g. ({ x: 1 })?.prop isn't even though { x: 1 } is by definition a subtype of unknown.

    Isn't this also true for any?

    This would lead to some subtle breakage in the future, e.g. let's say you wrote

    function fn(x: unknown) {
      if ('y' in x) {
        console.log(x?.z);
      }
    }

    I think you meant console.log(x?.y)?

    The proposal is to make this code OK, but then in the future it'd be a breaking change for us to narrow x to { y: unknown } in the body of the if, which seems bad - narrowing should only ever increase the set of things you're allowed to do.

    It does increase it from unknown to { y: T } which means we can safely do things like typeof x.y which wouldn't be possible on unknown.

  7. omeid commented on Dec 17, 2020

    @omeid

    For a concrete example, the famous react-router's Route component's children prop's location state can not be known and so it rightly sets the location param to Location<unknown>.

    Now referring to the state is pretty common, here is an example similar to the official documentation:

    function LoginPage() {
      const location = useLocation();
    
      const { from } = location.state || { from: "/" } };
      ....
      };

    Unpacking from with typescript when state is unknown becomes pretty cumbersome without allowing optional chaning to write:

    const { from } = typeof state?.from === "string" ? state : { from: "/" }`

    Or maybe there is a way to handle this simply without optional chaining on unknown?

  8. omeid commented on Dec 18, 2020

    @omeid

    Gonçalo Fernandes (@ilogico)

    function f(value: unknown): unknown {
      if (value != null) return value.someProp;
      return undefined;
    }

    Considering that this is possible with any, why shouldn't it be possible with unknown? At least with unknown unlike any, it is clear that nothing is known about the return value of this function and you must narrow the type before using it, unlike if it had returned any.

  9. RyanCavanaugh commented on Dec 18, 2020

    @RyanCavanaugh
    Member

    Considering that this is possible with any, why shouldn't it be possible with unknown?

    Why should two types designed for completely opposite purposes (any being the most-permissive, unknown the least) behave the same?

  10. ljharb commented on Dec 18, 2020

    @ljharb
    Contributor

    Ryan Cavanaugh (@RyanCavanaugh) becauae the entire purpose of ?. is to be able to navigate through all JS types - ie, x?.foo is guaranteed to always be unknown, in JS, when x is unknown. It may be that TS's design means that that's not a valid property to extract based on the type of the LHS and the name of the property on the RHS - but I'm not sure why that would result in anything but the same result as x.foo.

    In other words, the only difference between x.foo and x?.foo should be when x can be null or undefined - in which case, x?.foo results in undefined. As it is now, TS codebases are forced to use any instead of unknown when using optional chaining, which makes the code less type-safe.

  11. RyanCavanaugh commented on Dec 18, 2020

    @RyanCavanaugh
    Member

    Jordan Harband (@ljharb) I can't tell what you're trying to say with that.

    If you desugared

    function f(value: unknown): unknown {
      return value?.foo;
    }

    into

    function f(value: unknown): unknown {
      return value == null ? undefined : value.foo;
    }

    TypeScript would error on that, same as we would error on x.foo if x were object or { }. Are you saying it should error, or should not error?

    It seems like "New syntax constructs should behave like their desugarings" would be an uncontroversial interpretation but that's not what I'm hearing here.

  12. ljharb commented on Dec 18, 2020

    @ljharb
    Contributor

    Ryan Cavanaugh (@RyanCavanaugh) value.foo, specifically, would error, you're right, but let's say it was value?.toString - that shouldn't error - it'd either be undefined or a toString function.

  13. omeid commented on Dec 19, 2020

    @omeid

    Ryan Cavanaugh (@RyanCavanaugh)

    Considering that this is possible with any, why shouldn't it be possible with unknown?

    Why should two types designed for completely opposite purposes (any being the most-permissive, unknown the least) behave the same?

    any isn't actually more permissive, it is TypeScript's coercion rules that allows you to use it as any type you like. Both any and unknown indicate lack of type information.

    The reason the parallel is important is because as already mentioned, with unknown you can communicate the unsafe nature of your code and afford a degree of type-safety that isn't possible with any. So it is important to improve the ergonomics of working with unknown. This is consistent with TypeScript design goal of ergonomics over soundness.

  14. 24 remaining items

  15. hraban commented on Dec 24, 2021

    @hraban

    Based off the mention in #46314, I would guess that the potential bug would in the class of misspellings of properties i.e. a?.toStirng() would almost certainly be an error, but would not cause a run-time error leading to a difficult to find mistake. That's probably the best argument I've seen against it.

    Thanks for your reply, I just wanted to focus on the toStirng part: aren't typos in type declarations always a problem? If you force type declarations (e.g. Pimm "de Chinchilla" Hogeling (@Pimm)'s proposal) you end up shifting the typo from point of use to point of declaration.

    The comment you linked to (#46314) says short-circuiting would create the following risk:

    if (a?.foo) {
        a?.boo.yadda(); // oops
    }

    Maybe someone has a more realistic example, because this is precisely the kind of code you use optional chaining to avoid. You'd write:

    a?.foo.yadda();

    that's the point of null coalescing member access, as I understand it. But how would you do this currently? Cast to any? Same problem. Type casting to a custom type with explicit checking? Same problem, right?

    I guess my question is: Is there a solution to handling unknown in TS, currently, which is safer than optional chaining? I don't see optional chaining as a concession on any front, I'd love to understand what we'd be giving up.

  16. kralphs commented on Dec 24, 2021

    @kralphs

    Thanks for your reply, I just wanted to focus on the toStirng part: aren't typos in type declarations always a problem? If you force type declarations (e.g. Pimm "de Chinchilla" Hogeling (@Pimm)'s proposal) you end up shifting the typo from point of use to point of declaration.

    I was just saying that's the best argument against, not that I was a good argument against lol.

    I'm personally advocating for optional chaining to be allowed on unknown for mathematical reasons. Set theory, the basis of the type system itself, and the nature of the ?. operator seem to indicate optional chaining should be allowed. It both provides justification for its use, and a concrete ground to argue against it being expanded to other operators, thus avoiding the slippery slope that gets bandied about.

    The case of typos is a valid concern, but it becomes a question of what's more important: a logically consistent type system or avoiding a class of run-time errors. The type system is supposed to help avoid the latter, but does that make it less important? I don't think so, but others clearly disagree.

    But even the argument about avoidance of runtime errors has to be taken as what actually is going to prevent more run time errors, not what might. That ignores the human element. If more of the community stops casting to any because of optional chaining on unknown, that's safer code.

  17. Jcparkyn commented on Feb 8, 2022

    @Jcparkyn

    For those interested, you can somewhat emulate this behaviour like this:

    type SafeAny = {
      [key: PropertyKey]: SafeAny,
    } | undefined | null;
    
    const x = null as SafeAny;
    console.log(x?.children?.[0]?.age); // This is allowed
    console.log(x?.children[0]?.age); // This is an error

    The caveats here are:

    • You need to explicitly cast most types to SafeAny (... as unknown as SafeAny)
    • This doesn't allow function calls, with or without ?.().
    • Type narrowing is difficult, because typeof doesn't work correctly on this type (technically it is correct, but because we're lying to the compiler it makes the wrong assumptions). You can work around this by using type guards instead.
  18. Alexandre-Fernandez commented on Apr 29, 2022

    @Alexandre-Fernandez

    this is how I manage this problem (feel free to use it or improve it, for now it only works for one level deep properties) :

    export const getUnknownProperty = <T extends Typeof>(
      unknown: unknown,
      propertyKey: PropertyKey,
      typeofProperty: T
    ): TypeSafePropertyValue<T> | undefined => {
      if (!unknown) return undefined;
      if (typeof unknown !== "object") return undefined;
      const unknownAsRecord = unknown as Record<PropertyKey, unknown>;
      if (typeof unknownAsRecord?.[propertyKey] === typeofProperty) {
        return unknownAsRecord?.[propertyKey] as TypeSafePropertyValue<T>;
      }
      return undefined;
    };
    
    type TypeSafePropertyValue<T extends Typeof> = T extends "bigint"
      ? number
      : T extends "boolean"
      ? boolean
      : T extends "function"
      ? Function
      : T extends "number"
      ? number
      : T extends "object"
      ? object | null
      : T extends "string"
      ? string
      : T extends "symbol"
      ? symbol
      : undefined;
    
    type Typeof =
      | "bigint"
      | "boolean"
      | "function"
      | "number"
      | "object"
      | "string"
      | "symbol"
      | "undefined";
  19. yukinotech commented on Jul 28, 2022

    @yukinotech
    decodedData?.a?.b?.c; // Type is number | undefined

    decodedData?.a?.b?.c should be unknown in JSON.parse() ,or it is not safe

    const c = decodedData?.a?.b?.c  // Type is number | undefined
    if(c){
       c // number , but is it really a number type in js runtime ? 
    }
    
    type Unreliable<T> = T extends object
      ? {
          [key in keyof T]-?: Unreliable<T[key]>
        }
      : unknown
  20. adrian-gierakowski commented on Sep 30, 2022

    @adrian-gierakowski

    Thanks for this Alexandre Fernandez (@Alexandre-Fernandez)!

    I'm using it with a little tweak: instead of

      : T extends "object"
      ? object | null

    I use:

      : T extends "object"
      ?  { [key: string | number]: unknown } | null

    which makes the following safer:

    const prop = getPropFromUnknownType(someUnknown, 'prop', 'object')        
    const subProp = prop?.['subProp']

    When using object, subProp's type is implicitly assigned an any type (which is obviously unsafe) and causes an error when noImplicitAny is enabled.

  21. zanminkian commented on Aug 13, 2023

    @zanminkian
    function isTom(cat: unknown) {
      const firstName = cat?.name?.first
      if(typeof firstName === 'string' && firstName === 'Tom') return 'Yes'
      else return 'No'
    }
    isTom('Jerry') // No
    isTom(null) // No
    isTom(123) // No

    Code above will cause a compiling error in TS.
    To solve this problem, I have to change unknown to any, or use type assertion. But I believe we should not encourage to use any and type assertion in TS.

  22. NicholasAntidormi commented on Jan 28, 2025

    @NicholasAntidormi

    Disallowing optional chaining on unknown reduces its utility and forces unnecessary boilerplate. There are many common scenarios where this limitation becomes frustrating. For example:

      try {
        // some code
      } catch (err: unknown) {
        console.log(err?.message); // Not allowed
      }
      const response: unknown = await getResponse();
      const isOk = response?.status === 'ok'; // Not allowed
      if (isOk) ...
      const data: unknown = await getData();
      const nestedValue = data?.some?.nested?.value; // Not allowed
      if (isWhatINeed(nestedValue)) ...

    Is it possible to reconsider this issue?

  23. JuanGalilea commented on Nov 7, 2025

    @JuanGalilea

    Also it would solve issues like

    const someObject = { foo: 'bar' };
    const keys = Object.keys(someObject); // string[]
    
    for (const key of keys) {
        // error: No index signature with a parameter of type `string` was found on type `{ foo: string }`
        console.log(someObject?.[key]); 
    }

    being able to optional chain anything into any object would just reduce the painpoints caused by Object.keys not returning keyof type which Ryan Cavanaugh (@RyanCavanaugh) seems to have asked and answered in stackoverflow just to make it easier for devs struggling against this to find out why it is the way it is.

    Given that

    Object.keys returns keyof T

    is the one that was decided to not be true (which is good), at least a change like this would make the struggle of dealing with it less painful.

  24. grundb commented on Feb 11, 2026

    @grundb

    FWIW, here is a little helper function that just takes any value and performs a sequence of optional chaining property accesses on it (accepts any input and always produces unknown result):

    const optionalChain = (
      obj: unknown,
      path: string[]
    ): unknown => path.reduce((acc, prop) => (acc as any)?.[prop], obj);
  25. adrian-gierakowski commented on Feb 11, 2026

    @adrian-gierakowski

    FWIW, here is a little helper function that just takes any value and performs a sequence of optional chaining property accesses on it (accepts any input and always produces unknown result):

    const optionalChain = (
    obj: unknown,
    path: string[]
    ): unknown => path.reduce((acc, prop) => (acc as any)?.[prop], obj);

    That's a sledgehammer: you only want result to be unknown if you are chaining on an unknown.

  26. grundb commented on Feb 11, 2026

    @grundb

    FWIW, here is a little helper function that just takes any value and performs a sequence of optional chaining property accesses on it (accepts any input and always produces unknown result):

    const optionalChain = (
    obj: unknown,
    path: string[]
    ): unknown => path.reduce((acc, prop) => (acc as any)?.[prop], obj);

    That's a sledgehammer: you only want result to be unknown if you are chaining on an unknown.

    For sure. But it gets the job done for the common case of "I have an unknown thing and I want another unknown thing"

  27. adrian-gierakowski commented on Feb 11, 2026

    @adrian-gierakowski

    LLMs can do better:

    // 1. Recursive Type to traverse T based on keys in P
    type GetPath<T, P extends readonly string[]> = P extends
      [infer Key, ...infer Rest]
      ? Rest extends readonly string[] ? Key extends keyof NonNullable<T>
          // Found valid key: Recurse deeper
          // We add | undefined if the parent was nullable (simulating ?.)
          ?
            | GetPath<NonNullable<T>[Key], Rest>
            | (undefined extends T ? undefined : never)
            | (null extends T ? undefined : never)
          // Invalid key or T is unknown: Return unknown per requirements
        : unknown
      : never
      : T // End of path: return the resulting type
    
    // 2. The Implementation
    const optionalChain = <T, const P extends string[]>(
      obj: T,
      path: P,
    ): GetPath<T, P> => {
      // We cast the result because TS cannot verify the complex recursive reduction at runtime
      return path.reduce((acc, prop) => (acc as any)?.[prop], obj) as GetPath<T, P>
    }
    
    type User = {
      createdAt?: Date
      profile: {
        name: string
        age: unknown // Explicit unknown
        address?: {
          street: string
        }
      }
    }
    
    const user: User = {
      profile: { name: 'Alice', age: 25, address: { street: 'Liberation Street' } },
      createdAt: new Date(),
    }
    
    // 1. Happy Path
    export const name: string = optionalChain(user, ['profile', 'name'])
    export const createdAt: Date | undefined = optionalChain(user, ['createdAt'])
    export const street: string | undefined = optionalChain(user, [
      'profile',
      'address',
      'street',
    ])
    
    // 2. Path stops early (simulates optional chaining)
    export const profile: { name: string; age: unknown } = optionalChain(user, [
      'profile',
    ])
    
    // 3. Path hits 'unknown' property
    // Result: unknown (Because user.profile.age is unknown)
    export const age = optionalChain(user, ['profile', 'age'])
    
    // 4. Path continues THROUGH 'unknown'
    // Result: unknown (Because we can't infer keys of unknown)
    export const invalidDeep = optionalChain(user, ['profile', 'age', 'oops'])
    
    // 5. Path does not exist
    // Result: unknown
    export const badKey = optionalChain(user, ['profile', 'wrongKey'])

    the only problem is that you don't get autocomplete for path elements

  28. mfulton26 commented on Feb 11, 2026

    @mfulton26
    Contributor

    https://github.com/sindresorhus/dot-prop can be used to "get, set, or delete a property from a nested object using a dot path [or an array path]" that will return undefined for nonexistent paths.

  29. glebbash commented on Jun 3, 2026

    @glebbash

    Daniel Rosenwasser (@DanielRosenwasser) while I understand the architectural dead-ends and narrowing conflicts that led to this issue being closed, the underlying pain point remains a daily friction for many developers.

    I agree that making unknown?.field resolve to unknown globally could introduce subtle bugs, but the current strictness forces developers into unsafe type-casting (as any) for incredibly standard JavaScript patterns.

    Context

    Three quite common scenarios where this issue is a real pain is:

    NOTE: showing only the JavaScript versions. If you could show a good TypeScript solutions for these, then the issue should rightfully stay closed.

    1. Error handling

      Quite a lot of libraries set some kind of code on an error that you could use to check error type.

      try {
         // ....
      } catch (error) {
         if (error?.code !== Lib.RETRIABLE_ERROR_CODE) {
             throw error;   
         }
      
         // good to retry the call after a timeout
      }
    2. JSON.parse results handling in untyped apis

      const dataFromApi = JSON.parse(rawDataFromApi)
      const userName = dataFromApi?.user?.name
      if (typeof userName === "string") {
          // value is good to use
      }
    3. Libraries that accept unknown type

      const MYLIB_ERROR_TAG = Symbol("MyLibError")
      
      export function isMyLibErrorType(error) {
          return error?.[MYLIB_ERROR_TAG] === true
      }

    Possible solutions

    1. A restricted dynamic or loose_unknown type

      A type that behaves like exactly like unknown, but permits optional chaining/property access to yield further dynamic types. This avoids the total type-check shutdown of any.

    2. A localized compiler flag or block-level directive: "allowOptionalChainingOnUnknown": true or /* @ts-allow-in-next-block: unknown-optional-chaining */

      While tsconfig flags aren't the best looking solution here (although this might be the simplest option to implement), allowing a block-scoped directive (similar to ESLint rules) to permit optional chaining on unknown could localize the risk to just those error-handling or json handling blocks.

    There definitely is a middle ground where we don't have to choose between writing verbose (and sometimes runtime affecting) type-guards or completely surrendering to any just to safely read a nested property.

    I hope this issue can be re-evaluated, I think enough people has raised a voice around this to signal that its often a real pain, especially in codebases that weren't written with TypeScript in mind and are gradually trying to get more type safety without testing every single change after typing existing code, because getting rid of anys quite often requires runtime logic changes that are only needed to satisfy the TS compiler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions