Skip to content
New issue

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

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

Already on GitHub? Sign in 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.

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

No branches or pull requests

2 participants