[Bug] Excess properties are allowed when using `reactive()`
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
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.
you should:
// 👇 const a2: A = reactive<A>({ a: 'a', b: '', // 👈 thrown })
@edison1105 This usage is officially discouraged.
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
})
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:
@LinusBorg Could StrictUnion help?
Since were not dealing with a Union Type, I don't see how. I'm not one of the TS experts here, though
@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.