Spread operator in TypeScript is not sound!!

When I was building MigaCSS, I met a bug that I wondered why TypeScript couldn’t catch it.

Here is some code illustrating the issue.

tsx
import React from 'react'
 
function A() {
const props = {
onClick: () => {},
jser: 'dev'
}
 
return <B {...props}/>
}
 
function B(props: {onClick:() => void}) {
return <div {...props}/>
}
tsx
import React from 'react'
 
function A() {
const props = {
onClick: () => {},
jser: 'dev'
}
 
return <B {...props}/>
}
 
function B(props: {onClick:() => void}) {
return <div {...props}/>
}

I accidentally passed down props to child components, "jser" is not on B but TypeScript doesn’t complain.

I’d expect it to be complained because it does if not with spread.

tsx
import React from 'react'
 
function A() {
const props = {
onClick: () => {},
jser: 'dev'
}
 
return <B onClick={props.onClick} jser={props.jser}/>
Type '{ onClick: () => void; jser: string; }' is not assignable to type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'. Property 'jser' does not exist on type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'.2322Type '{ onClick: () => void; jser: string; }' is not assignable to type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'. Property 'jser' does not exist on type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'.
}
 
function B(props: React.ComponentProps<'div'>) {
return <div {...props}/>
}
tsx
import React from 'react'
 
function A() {
const props = {
onClick: () => {},
jser: 'dev'
}
 
return <B onClick={props.onClick} jser={props.jser}/>
Type '{ onClick: () => void; jser: string; }' is not assignable to type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'. Property 'jser' does not exist on type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'.2322Type '{ onClick: () => void; jser: string; }' is not assignable to type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'. Property 'jser' does not exist on type 'IntrinsicAttributes & ClassAttributes<HTMLDivElement> & HTMLAttributes<HTMLDivElement>'.
}
 
function B(props: React.ComponentProps<'div'>) {
return <div {...props}/>
}

Considering JSX is merely a syntax suger, we can simplify the repro code as below.

ts
type A = {
a: string
}
 
const a: A = {
a: 'jser',
b: 'dev'
Type '{ a: string; b: string; }' is not assignable to type 'A'. Object literal may only specify known properties, and 'b' does not exist in type 'A'.2322Type '{ a: string; b: string; }' is not assignable to type 'A'. Object literal may only specify known properties, and 'b' does not exist in type 'A'.
}
 
const b = {
a: 'jser',
b: 'dev'
}
 
// Why doesn't following code get complained?
const c: A = {
...b
}
 
ts
type A = {
a: string
}
 
const a: A = {
a: 'jser',
b: 'dev'
Type '{ a: string; b: string; }' is not assignable to type 'A'. Object literal may only specify known properties, and 'b' does not exist in type 'A'.2322Type '{ a: string; b: string; }' is not assignable to type 'A'. Object literal may only specify known properties, and 'b' does not exist in type 'A'.
}
 
const b = {
a: 'jser',
b: 'dev'
}
 
// Why doesn't following code get complained?
const c: A = {
...b
}
 

After doing a little search I found this issue, and got to know that this is by design.

When you spread in c, you don’t know what properties it really has. So TypeScript doesn’t really know if you have excess properties in some cases.

Yeah, I don’t know much about the TypeScript internals, but Flow does a great job alerting on this issue.

Before TypeScript improves on this issue, just remember that spread leads to unsound type in TypeScript, it is best to explicit type the variable before spreading.

tsx
import React from 'react'
 
function A() {
const props: React.ComponentProps<typeof B> = {
onClick: () => {},
jser: 'dev'
Type '{ onClick: () => void; jser: string; }' is not assignable to type '{ onClick: () => void; }'. Object literal may only specify known properties, and 'jser' does not exist in type '{ onClick: () => void; }'.2322Type '{ onClick: () => void; jser: string; }' is not assignable to type '{ onClick: () => void; }'. Object literal may only specify known properties, and 'jser' does not exist in type '{ onClick: () => void; }'.
}
 
return <B {...props}/>
}
 
function B(props: {onClick:() => void}) {
return <div {...props}/>
}
tsx
import React from 'react'
 
function A() {
const props: React.ComponentProps<typeof B> = {
onClick: () => {},
jser: 'dev'
Type '{ onClick: () => void; jser: string; }' is not assignable to type '{ onClick: () => void; }'. Object literal may only specify known properties, and 'jser' does not exist in type '{ onClick: () => void; }'.2322Type '{ onClick: () => void; jser: string; }' is not assignable to type '{ onClick: () => void; }'. Object literal may only specify known properties, and 'jser' does not exist in type '{ onClick: () => void; }'.
}
 
return <B {...props}/>
}
 
function B(props: {onClick:() => void}) {
return <div {...props}/>
}

😳 Would you like to share my post to more people ?    

❮ Prev: A real example of concurrent mode in Shaku - switch to `useDeferredValue()` instead of throttling or debouncing.

Next: How UnoCSS works internally with Vite?