Learn why TypeScript throws TS2367 when two compared types have no overlap, and how to fix mismatched primitives, narrowed variables and enum checks.
TS2367 is TypeScript telling you that a comparison can never be true. It looks at the static types of the left and right operand of ===, !==, == or !=, finds that neither type is comparable to the other, and concludes the whole expression is a constant — so you probably did not mean to write it.
The check has no flag behind it: the compiler runs it for every equality comparison, and the types it uses are the narrowed types at that point in the code. That last part is why TS2367 so often shows up on a line that looks perfectly reasonable — control-flow analysis has already decided the variable can only hold one literal.
Note that TypeScript is not claiming your code crashes. At runtime "5" == 5 is true and comparing a Date to a string works fine. The compiler is saying: given the types you declared, this branch is dead code.
// The general shape of the error:
// This comparison appears to be unintentional because the types 'string' and 'number' have no overlap.
// ~~~~~~ ~~~~~~
// left operand right operandThe wording changed twice. Before TypeScript 3.0 the code 2367 carried an unrelated message, from 3.0 to 4.8 it read "This condition will always return 'false' since the types 'X' and 'Y' have no overlap", and 4.9 rephrased it as "appears to be unintentional" — the same check, friendlier wording. If you search for the old sentence you will find answers that still apply.
The classic case: an id that arrives as a string from a route parameter, compared with a numeric literal. Or the reverse — a number compared with a numeric string.
// ❌ Broken
declare const userId: string
declare const items: string[]
if (userId === 42) {
// ~~~~~~~~~~~~~ Error: This comparison appears to be unintentional because the types 'string' and 'number' have no overlap.
}
if (items.length === "0") {
// ~~~~~~~~~~~~~~~~~~~ Error: This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.
}// ✅ Fixed — compare like with like, converting explicitly where needed
declare const userId: string
declare const items: string[]
if (Number(userId) === 42) {
}
if (items.length === 0) {
}If the id really is a string, compare with a string (userId === "42"); if it is numeric data in string form, convert it. Reach for Number() or String() rather than loosening the comparison to ==. Loose equality silences the error for "42" == 42, but it also makes 0 == "" true, which is rarely what you want.
This one surprises everyone. When you write let state: RequestState = "idle", TypeScript narrows state to the literal type "idle" for the rest of the flow — and it does not track assignments that happen inside a function you call.
// ❌ Broken
type RequestState = "idle" | "loading" | "done"
let state: RequestState = "idle"
function startRequest() {
state = "loading"
}
startRequest()
if (state === "loading") {
// ~~~~~~~~~~~~~~~~~ Error: This comparison appears to be unintentional because the types '"idle"' and '"loading"' have no overlap.
}The fix is to make the mutation visible to the compiler. Returning the new state from the helper is the cleanest option; keeping the value on an object property also works, because property types are never narrowed by a call you make in between.
// ✅ Fixed — the assignment happens in the same flow the compiler can see
type RequestState = "idle" | "loading" | "done"
function startRequest(): RequestState {
return "loading"
}
let state: RequestState = "idle"
state = startRequest()
if (state === "loading") {
}Enum values are not interchangeable with the literals behind them. Since TypeScript 5.0 a string enum compares cleanly with a string literal that matches one of its members, but a literal that matches no member — a typo, a renamed member, a value from an older API — still has no overlap. Mixing kinds (a string enum against a number) never works.
// ❌ Broken
enum UserRole {
Admin = "admin",
Editor = "editor",
}
declare const role: UserRole
if (role === "superadmin") {
// ~~~~~~~~~~~~~~~~~~~~ Error: This comparison appears to be unintentional because the types 'UserRole' and '"superadmin"' have no overlap.
}
if (UserRole.Admin === 1) {
// ~~~~~~~~~~~~~~~~~~~ Error: This comparison appears to be unintentional because the types 'UserRole' and 'number' have no overlap.
}// ✅ Fixed — compare against the member, so renames stay type-safe
enum UserRole {
Admin = "admin",
Editor = "editor",
}
declare const role: UserRole
if (role === UserRole.Admin) {
}Quite often TS2367 is simply right and has found a bug. An && that should be ||, a comparison against an object instead of one of its properties, or a misspelled typeof tag all produce two types with no overlap.
// ❌ Broken
declare const shippingCountry: string
declare const orderDate: Date
if (shippingCountry === "DE" && shippingCountry === "AT") {
// ~~~~~~~~~~~~~~~~~~~~~~~~~ Error: the types '"DE"' and '"AT"' have no overlap.
}
if (typeof orderDate === "objcet") {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Error: the types '"string" | "number" | ... | "function"' and '"objcet"' have no overlap.
}
if (orderDate == "2024") {
// ~~~~~~~~~~~~~~~~~~ Error: the types 'Date' and 'string' have no overlap.
}// ✅ Fixed — the comparisons people actually meant
declare const shippingCountry: string
declare const orderDate: Date
if (shippingCountry === "DE" || shippingCountry === "AT") {
}
if (typeof orderDate === "object") {
}
if (orderDate.getFullYear() === 2024) {
}Notice the second case: the && chain narrows shippingCountry to "DE" before the right-hand side runs, which is exactly how the compiler arrives at "DE" and "AT".
Read both type names in the message. TypeScript prints the narrowed types, not the declared ones. If the left side reads '"idle"' instead of your union, narrowing is the real subject of the error, not the comparison.
Assume the compiler found a bug first. Check for an && that should be ||, a missing property access (config instead of config.mode), a typo in a typeof tag, or a value compared against a member name that no longer exists.
Convert explicitly when the types genuinely differ. Use Number(value), String(value) or value.toString() so the comparison is between two values of the same type. Keep === — switching to == hides the mismatch instead of resolving it.
Make reassignments visible when narrowing is the cause. Return the new value from the helper that produces it, or keep the value on an object property or in a store. Both keep the declared union alive at the comparison.
Use the enum member, never its raw value. role === UserRole.Admin keeps working when the member's value changes; role === "admin" silently stops matching.
Only assert as a last resort. (state as RequestState) === "loading" compiles, but it tells the compiler to forget what it knows about your control flow. If you reach for it, leave a comment explaining why the narrowing is wrong — and never use as any, which removes every other check on that line as well.
TS2367 fires when you compare two values with ===, !==, == or != and their static types have nothing in common, so the result is a constant. Typical causes are comparing a string to a number, comparing an enum with a literal that is not one of its members, or comparing a variable that control-flow analysis has already narrowed to a single literal.
The check is purely about types. TypeScript is not predicting a crash — it is pointing out that under the types you declared, the branch can never be taken, which usually means either the types or the comparison are wrong.
Because TypeScript narrows a variable to the literal type you assigned and does not track reassignments that happen inside another function.
type ConnectionState = "offline" | "online"
let connection: ConnectionState = "offline" // narrowed to '"offline"'
function connect() {
connection = "online" // the compiler does not replay this at the call site
}
connect()
if (connection === "online") {
// Error: the types '"offline"' and '"online"' have no overlap.
}Move the value onto an object property (const socket = { connection: "offline" as ConnectionState }) or return the new state from connect(). Both keep the declared union at the comparison, which is also the honest description of what the code does.
Compare against the enum member itself:
enum PaymentStatus {
Paid = "paid",
Refunded = "refunded",
}
declare const paymentStatus: PaymentStatus
if (paymentStatus === PaymentStatus.Paid) {
}Since TypeScript 5.0, paymentStatus === "paid" is also accepted, because string enum members behave like literal types. A string that matches no member — "settled", or an old value you renamed — still raises TS2367, and so does comparing a string enum with a number. Using the member is the version that survives a rename, and it is what the error is nudging you towards.
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