Skip to content

Unexpected "property is not assignable to type 'undefined'" on dynamic assignmentΒ #61409

Description

πŸ”Ž Search Terms

strictNullChecks "not assignable" "to type 'undefined'"

πŸ•— Version & Regression Information

  • This changed between versions 3.3.3 and 3.5.1
    All newer versions have this behavior

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/JYOwLgpgTgZghgYwgAgHIHsAmEAKV0AOAzsgN4BQyVycA-AFzIgCuAtgEbQDcl17DyImCigA5jwC+5cgnQghydCNGg4AG0YZseQiQC8ZCT1nywyMHCiiIYTVlz5iyA6SPS1N5AUcBGRgGsIAE90GDR7HScDAHI4aJ4LKxsAbW9CHwBdZ0VlVTVU3wyuZAB6EuQAdyV-IndPNIIAJgDg0PDtR31kWOiaEkCQsK0HXQTLazACwkasgyVgFRB1Kaai0vKAUSh8KGkTBQaiRgBBbbgggB4BtuHIogA+bOSi8hglZAAKfbMGxTDDgCUZF4VESExWsxyCzyEOKZWQWx25CkQA

πŸ’» Code

interface NodeProps {
    a?: number;
    b?: string;
}

const original: NodeProps = {};
const target: NodeProps = {};

let prop1: keyof NodeProps = 'a';
target[prop1] = original[prop1]; // works

let prop2: keyof NodeProps = 'a' as keyof NodeProps;
target[prop2] = original[prop2]; // Error

const props: Array<keyof NodeProps> = [];
for (const prop of props) {
    target[prop] = original[prop]; // Error
}

πŸ™ Actual behavior

Got unexpected errors:

Type 'string | number | undefined' is not assignable to type 'undefined'.
  Type 'string' is not assignable to type 'undefined'.

πŸ™‚ Expected behavior

Should produce no errors

Additional information about the issue

Seems to be closely related to #59969 - disabling strictNullChecks option changes the error text, but essentially error stays the same.

Error produced with strictNullChecks:

Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.

Activity

  1. nmain commented on Mar 12, 2025

    @nmain

    This is intended behavior, and was changed in #30769 and documented as a breaking change in https://github.com/microsoft/TypeScript/wiki/Breaking-Changes#fixes-to-unsound-writes-to-indexed-access-types. The compiler only allows it when it can prove the key type is just the unit "a" and not "a" | "b".

  2. DimaIT commented on Mar 12, 2025

    @DimaIT
    Author

    An error would be expected if we try to set the value of potentially different type, as described in the link you've provided.
    But in my example value types match, therefore it's not clear to me why such behavior is expected.

    interface A {
        a?: number;
        b?: string;
    }
    
    const original: A = {};
    const target: A = {};
    
    let prop = "a" as "a" | "b";
    target[prop] = 10; // Error - expected
    target[prop] = "str"; // Error - expected
    target[prop] = original[prop]; // Error - why?

    UPD:
    Also the issue is gone if we do exact same assignment, only inside a generic function:

    function write<K extends keyof A>(arg: A, key: K, value: A[K]): void {
        arg[key] = value;
    }
    write(target, prop, original[prop]); // now it works
  3. MartinJohns commented on Mar 12, 2025

    @MartinJohns
    Contributor

    Also the issue is gone if we do exact same assignment, only inside a generic function:

    Be aware that this is (intentionally) unsound and allows invalid assignments: write<"a" | "b">({}, "a", "abc")

  4. nmain commented on Mar 12, 2025

    @nmain

    But in my example...

    There's quite a bit of explanation in #30769, and in the many linked PRs from that, that explain this all in more detail.

    Also the issue is gone if...

    I'm not certain, but I believe this is a separate (although somewhat related) soundness issue. The type A[K] is evaluated as if it were a read, but then is allowed for a property write. Property writes are sometimes treated covariantly when they shouldn't be.

    The existence of one soundness hole does not imply that all soundness should be removed.

  5. DimaIT commented on Mar 12, 2025

    @DimaIT
    Author

    There are some nice explanations indeed:

    As human beings, we know if the key is the same, so will the type retrieved, regardless of what types are involved, but this doesn’t automatically follow from the compiler’s perspective unless the compiler is explicitly programmed to track it.

    Even a workaround was proposed, but not implemented.

  6. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions