Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

How should we interpret the compatibility between optional-never objects and normal objects? #61396

Closed
5 of 6 tasks
horita-yuya opened this issue Mar 11, 2025 · 2 comments
Closed
5 of 6 tasks

Comments

@horita-yuya
Copy link

horita-yuya commented Mar 11, 2025

🔍 Search Terms

Introduction:

Consider the following TypeScript types:

type Ob1 = {
  a: string,
  b?: never
}

type Ob2 = {
  a: string,
}

type Ob3 = {
  a: string,
  b?: number
}

type Ob4 = {
  a: string,
  b: number
}

type Ob5 = {
  a: string,
  b?: undefined
}

type Ob6 = {
  a: string,
  b?: unknown
}

declare let n1: Ob1
declare let n2: Ob2
declare let n3: Ob3
declare let n4: Ob4
declare let n5: Ob5
declare let n6: Ob6

// OK
let case1: Ob1 = n2
let case1_1: Ob2 = n1

// Error
let case2: Ob1 = n3
let case3: Ob1 = n4
// Error if exactOptionalPropertyTypes=true
let case4: Ob1 = n5
let case5: Ob1 = n6

Is the compatibility between types Ob1 and Ob2 part of an intended trade-off within TypeScript's type system?

https://www.typescriptlang.org/play/?exactOptionalPropertyTypes=true#code/C4TwDgpgBA8gRgRigXigbwFBSgQwFxQDOwATgJYB2A5gDRZRwD8BFEAbhCRgL4YaiRYcAEwp09fEVKVaPPgOjwAzGMzZJxctTrYmLAK4BbOJzn9wiuABZVEgppk6GB46d7nB8AKy3196dr0elD6FAAmEABmlBBhZgpCAGy+uP5asrrMIRQA1hQA9gDuFGYRAMYANjgk0BUQwFAUCATwCBjlVTVQdQ0Uwi0i7RCV1bX1jUoDSkMjXT2NVgNWM51jvV4DXiuj3eMUiQOJfAD0x7AA0hjzZTiEEM1CSKh9V+M3dwgA+g-wos9tGFOUAAoiQSPkuNdbhB+o8xBRplC7pM4c9lkDQeCSFAyJEoBAAB44MrAGBgYBkfIUHAVAAK4MgJFAABULIRkKR9BBXg13hBFqjGlskRANoL9kA

Or,

✅ Viability Checklist

⭐ Suggestion

Introduce stricter type checking for optional never to prevent runtime errors, such as:

const funcB = (params: { b: string }) => {
  const result = funcA(params) // Type Check Error expected here
  console.log("Result Length: ", result.length)
}

📃 Motivating Example

The inconsistency above can lead to unintended runtime errors. For instance:

type Params = { a?: never, b: string } | { a: string, c: string }

const funcA = (params: Params): string => {
  if (params.a !== undefined) {
    return params.c
  } else {
    return params.b
  }
}

const funcB = (params: { b: string }) => {
  const result = funcA(params)
  console.log("Result Length: ", result.length) // runtime error
}

const params = { a: 'a', b: 'b' }
funcB(params)

https://www.typescriptlang.org/play/?#code/C4TwDgpgBACghgJzgWwM5QLxQN5TgfgC4oA7CANwgQBooAjY1YBASxIHMoBfKAHxzyNmbdrQDGQ1h24AoGWID2JJlABmAVxJiAgpigAKMIhSpi8JGgCUkkZgB8OGVCgtVBoxdQA6OFACEGFiaACYQqmwQwZaOzs4IEMDqCCRQHiZeYk7cUBAANqjQ2FlxCUkpaWhedFlcMrXySioaWgBCeobGaMS4DFBMUpxc0RgORc6KysBQ8ajquVNYzTodnpZZE6gKuRBeuQrs+gBEAEoQs-NQADIQHMAAFsSHtDNzwLs37PfRAPTf05rAFjIaBUBAKBB1OQbKYVdBYXBwYgAcjgSNovSRdCRsiWLRWJksQA

The code compiles without errors but causes runtime issues.

💻 Use Cases

  1. What do you want to use this for?
  2. What shortcomings exist with current approaches?
  3. What workarounds are you using in the meantime?
@jcalz
Copy link
Contributor

jcalz commented Mar 11, 2025

This is a known unsoundness, intentionally allowed. Approximately nobody wants the following code to produce an error:

function foo(arg: { a: number, b?: string }) {
  console.log(arg.a.toFixed(), arg.b?.toUpperCase())
}

const x = { a: 123 };
foo(x); // <-- ERROR?! We don't know that x is lacking the b property

See #42479 (assignment transitivity is broken), #13043 (for parameters but it's the same issue), #40529 (for index signatures but it's the same issue), #12936 (the exact types feature that would be needed to possibly improve this situation, so typeof x would know that a is the only property).

✅ This wouldn't be a breaking change in existing TypeScript/JavaScript code

This would most certainly break a bunch of existing TypeScript code; soundness improvements almost always do.

@horita-yuya
Copy link
Author

horita-yuya commented Mar 11, 2025

Thanks a lot.

type A = { b: string };
type B = { a?: never; b: string };

The compatibility of objects is determined by keyof. Since A only has the key "b", compatibility is decided based on this key alone.

For B -> A, the key "a" in B has the type never. Since never is assignable to all types that include never, this doesn’t affect assignability.

As a result, A and B are mutually assignable.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants