Subscribe on changes!

[Bug] Excess properties are allowed when using `reactive()`

avatar
Mar 3rd 2023

Vue version

3.2.47

Link to minimal reproduction

https://github.com/wenfangdu/vite-vue-3/tree/reactive-type

Steps to reproduce

interface A {
  a: string
}
const a1: A = {
  a: 'a',
  b: '', // 👈 thrown
}
const a2: A = reactive({
  a: 'a',
  b: '', // 👈 not thrown
})

What is expected?

const a2: A = reactive({
  a: 'a',
  b: '', // 👈 thrown
})

What is actually happening?

const a2: A = reactive({
  a: 'a',
  b: '', // 👈 not thrown
})

System Info

System:
  OS: macOS 13.2.1
  CPU: (8) arm64 Apple M1
  Memory: 96.98 MB / 16.00 GB
  Shell: 5.2.12 - /opt/homebrew/bin/bash
Binaries:
  Node: 16.18.0 - ~/.nvm/versions/node/v16.18.0/bin/node
  Yarn: 1.22.19 - ~/.nvm/versions/node/v16.18.0/bin/yarn
  npm: 9.5.1 - ~/.nvm/versions/node/v16.18.0/bin/npm
Browsers:
  Chrome: 110.0.5481.177
  Safari: 16.3
npmPackages:
  vue: ^3.2.20 => 3.2.47

Any additional comments?

No response

avatar
Mar 3rd 2023

reactive will automatically infer its type based on the incoming object

avatar
Mar 3rd 2023

you should:

//                    👇      
const a2: A = reactive<A>({
  a: 'a',
  b: '', // 👈 thrown
})
avatar
Mar 3rd 2023

reactive will automatically infer its type based on the incoming object

@baiwusanyu-c I simplified the object for minimal repro, the actual object is fairly complex.

avatar
Mar 3rd 2023

you should:

//                    👇
const a2: A = reactive<A>({
  a: 'a',
  b: '', // 👈 thrown
})

@edison1105 This usage is officially discouraged.

image

Also, it doesn't work well with the following scenario:

interface A {
  a(): void
}

const a1: A = reactive({
  a: () => (a1.a = ''), // 👈 thrown
})

const a2 = reactive<A>({
  a: () => (a2.a = ''), // 👈 not thrown
})
avatar
Mar 3rd 2023

This seems to be a TS bug (or at least: a typically weird TS edge case). Here's a demo of what's happening in the TS Playground:

function reactive<T extends object>(obj: T): T {
    return obj
}

interface A {
  a: string
}
const a1: A = {
  a: 'a',
  b: '', // 👈 thrown
}

// For some reason, TS is fine with this, even through 
// reactive() here returns { a: string, b: string }
// (see mouseover in the playground)
const a2: A = reactive({
  a: 'a',
  b: '', // 👈 not thrown
})

// A better route forward could be `satisfies`, which is availabelk since TS 4.9
const a3 = reactive({
  a: 'a',
  b: '', // 👈 thrown
} satisfies A)

I'm not aware of how we could change the generics inference in reactive to make the type assertion work as desired. Anyone?

Edit: This seems to be about kind of the same problem and indeed points to this being how TS works at a fundamental level:

https://stackoverflow.com/questions/58864033/in-typescript-is-there-a-way-to-restrict-extra-excess-properties-for-a-partial

avatar
Mar 3rd 2023

@LinusBorg Could StrictUnion help?

avatar
Mar 3rd 2023

Since were not dealing with a Union Type, I don't see how. I'm not one of the TS experts here, though

avatar
Mar 12th 2023

@LinusBorg satisfies works in this case:

interface A {
  a: string
}
const a1: A = {
  a: 'a',
  b: '', // 👈 thrown
}
const a2 = reactive({
  a: 'a',
  b: '', // 👈 thrown
} satisfies A)

But it doesn't work in this case:

interface A {
  a(): void
}
const a1: A = reactive({
  a: () => (a1.a = ''), // 👈 thrown
})
const a2 = reactive({
  a: () => (a2.a = ''), // 👈 not thrown
} satisfies A)

For now, the only way I know to make both cases thrown is this:

interface A {
  a(): void
}
const a1: A = reactive({
  a: () => (a1.a = ''), // 👈 thrown
  b: '', // 👈 not thrown
})
const a2: A = reactive<A>({
  a: () => (a2.a = ''), // 👈 thrown
  b: '', // 👈 thrown
})

But const a2: A = reactive<A> is repetitive and against the official guideline.

avatar
Mar 13th 2023

So I think that settles it - this is just a limitation of how TS works.