Learn why TypeScript throws TS2739 when a value is missing several required properties of its target type, and how to fix it without casting.
TypeScript error TS2739 means a value does not have all the properties its target type requires — and that more than one property is missing. The compiler compares the two shapes, finds the gaps, and lists them by name:
const profile: UserProfile = fromApi
// ~~~~~~~
// Type 'UserProfileDto' is missing the following properties
// from type 'UserProfile': displayName, createdAtThis is TypeScript's structural type system doing its job. A type is not a label you attach to a value; it is a contract about which properties the value has. UserProfileDto and UserProfile are unrelated as far as the compiler is concerned — all that matters is that the DTO lacks displayName and createdAt, so it cannot stand in for a UserProfile.
The number of missing properties decides which code you see. One missing property is TS2741 (Property 'X' is missing in type 'Y' but required in type 'Z'), two to five are listed by name as TS2739, and from six onwards the compiler truncates the list and switches to TS2740 (…: a, b, c, d, and 2 more). Same check, same fixes — only the wording of the report changes.
The most common source is a variable that is annotated with its final type but initialised empty, with the intention of filling it in later. The annotation is a promise the empty literal cannot keep.
interface ChartOptions {
width: number
height: number
theme: "light" | "dark"
}
// ❌ Broken
const chartOptions: ChartOptions = {}
// ~~~~~~~~~~~~
// Error: Type '{}' is missing the following properties from type
// 'ChartOptions': width, height, theme (TS2739)
chartOptions.width = 640
chartOptions.height = 360The assignments on the following lines do not help: TypeScript checks the initialiser at the point of assignment, not what you intend to do with the variable afterwards.
// ✅ Fixed — "not configured yet" is a real state, so put it in the type
let chartOptions: ChartOptions | null = null
function configureChart(width: number, height: number) {
chartOptions = { width, height, theme: "light" }
}Now every reader of chartOptions is forced to handle the null case, which is exactly the bug the empty object was hiding.
Backend DTOs rarely match domain models one to one. Assigning one to the other works until the domain type grows a field the payload does not carry.
interface UserProfileDto {
id: string
email: string
}
interface UserProfile {
id: string
email: string
displayName: string
createdAt: Date
}
const fromApi: UserProfileDto = { id: "u_1842", email: "ada@example.com" }
// ❌ Broken
const profile: UserProfile = fromApi
// ~~~~~~~
// Error: Type 'UserProfileDto' is missing the following properties from
// type 'UserProfile': displayName, createdAt (TS2739)// ✅ Fixed — map the transport shape to the domain shape explicitly
function toUserProfile(dto: UserProfileDto): UserProfile {
return {
id: dto.id,
email: dto.email,
displayName: dto.email.split("@")[0],
createdAt: new Date(),
}
}
const profile: UserProfile = toUserProfile(fromApi)A mapper function is more code than a cast, and it is the only version that still compiles when someone adds a fifth field to UserProfile: the error then points at toUserProfile, which is where the decision belongs.
In JSX the props object is checked against the props type, so forgetting required props produces the same error — with the missing prop names listed.
interface InvoiceCardProps {
invoiceId: string
amount: number
currency: string
onOpen: () => void
}
function InvoiceCard({ invoiceId, amount, currency, onOpen }: InvoiceCardProps) {
return (
<button onClick={onOpen}>
{invoiceId}: {amount} {currency}
</button>
)
}
export function InvoiceList() {
// ❌ Broken
return <InvoiceCard invoiceId="inv_42" />
// ~~~~~~~~~~~
// Error: Type '{ invoiceId: string; }' is missing the following properties
// from type 'InvoiceCardProps': amount, currency, onOpen (TS2739)
}// ✅ Fixed — pass every required prop
export function InvoiceList() {
return (
<InvoiceCard
invoiceId="inv_42"
amount={128.5}
currency="EUR"
onOpen={() => console.log("open inv_42")}
/>
)
}If a prop is genuinely optional, say so in the props type (currency?: string) and give it a default in the component — do not force callers to pass a placeholder value.
Test doubles and in-memory fakes are the other classic case: the stub implements the one method the test exercises, and the compiler points out the rest of the contract.
interface Order {
id: string
total: number
}
interface OrderRepository {
findById(id: string): Promise<Order | null>
save(order: Order): Promise<void>
delete(id: string): Promise<void>
}
// ❌ Broken
const fakeRepository: OrderRepository = {
// ~~~~~~~~~~~~~~
// Error: Type '{ findById: () => Promise<{ id: string; total: number; }>; }'
// is missing the following properties from type 'OrderRepository':
// save, delete (TS2739)
findById: async () => ({ id: "ord_1", total: 42 }),
}// ✅ Fixed — either complete the stub...
const fakeRepository: OrderRepository = {
findById: async () => ({ id: "ord_1", total: 42 }),
save: async () => undefined,
delete: async () => undefined,
}
// ✅ ...or narrow the contract the consumer actually needs
type OrderReader = Pick<OrderRepository, "findById">
const readOnlyRepository: OrderReader = {
findById: async () => ({ id: "ord_1", total: 42 }),
}The second fix is usually the better one: if a function only reads orders, typing its parameter as OrderReader makes every future stub smaller and documents the dependency honestly.
Read the property list in the message. TypeScript names every missing property. Nine times out of ten the fastest fix is to supply those properties with real values — the error is a genuine gap, not a type-system quirk.
Model "not ready yet" with null, not with an empty object. A variable that will be filled in later is ChartOptions | null, never ChartOptions initialised to an empty literal. The union forces the rest of the code to handle the empty state instead of crashing at runtime on undefined.
Convert between shapes with a mapper function. When the source is a DTO, a form value, or a third-party payload, write a toDomainType() function that fills in the missing fields. It gives you one place to change when either shape evolves.
Relax the target type only when the properties really are optional. Mark them with ? (avatarUrl?: string), or accept Partial<T> in a function that updates rather than constructs. Use Pick<T, K> to ask for the narrow contract you actually consume.
Do not silence it with a cast. fromApi as UserProfile or {} as ChartOptions compiles, but every later read of createdAt or theme gets undefined at runtime while the type says otherwise. If you must bridge an untyped boundary, validate the payload at that boundary and keep the rest of the code honestly typed.
TS2739 fires when you assign a value to something whose type requires properties the value does not have, and two or more of those properties are missing. TypeScript checks shapes structurally, so it does not matter whether the names of the two types look related — only whether every required property is present. The message lists the missing names, which makes the error unusually easy to act on. The usual sources are empty object literals, partial API payloads, React components rendered without required props, and incomplete test stubs.
Stop annotating the empty literal with the full type. If the value is genuinely built in one place, construct it with every required property at once. If it is filled in later, type the variable as ChartOptions | null and start at null — the compiler then forces every reader to deal with the unconfigured state, which is the bug the empty object was hiding. Partial<ChartOptions> is the right type only while the value is still incomplete, and you should convert it to the full type in one checked step before handing it to code that expects all the fields.
All three come from the same structural assignability check and differ only in how much detail fits in the message. TS2741 reports a single missing property (Property 'theme' is missing in type '…' but required in type 'ChartOptions'), TS2739 lists two to five missing properties by name, and TS2740 appears when more than five are missing and truncates the list with and N more. Seeing TS2740 instead of TS2739 therefore tells you the shape mismatch is large — often a completely different type, not a nearly-complete object. The fixes are identical: supply the properties, map between the shapes, or narrow the target type.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges