DEV Community

Skyler
Skyler

Posted on

Offensive Coding: Exploiting JavaScript's Tiny Door

Offensive Coding: The Four-Line Function TypeScript Says Is Broken And Isn't

The one line

Every dev writes this without thinking:

function readValue<T>(s: T | undefined): Promise<T> {
  return s ? s : undefined
}
Enter fullscreen mode Exit fullscreen mode

The : Promise<T>. The s ? s : undefined. Both reflexive. Both feel correct. Both close a door that has been open since async/await shipped.

Here's the door open:

function MaybePromise(value) {
  return value !== undefined ? value : Promise.resolve(value)
}
Enter fullscreen mode Exit fullscreen mode

Four lines. No async. No await. No types. And it does something TypeScript insists is impossible.

The door

JavaScript does not see await.

await is a contextual keyword. It's only the await operator inside a function explicitly marked async. Outside that context, it's an identifier. A variable name.

function await(x) { return 'awaiting ' + x }
function foo() { return await(42) }  // legal
Enter fullscreen mode Exit fullscreen mode

That's why async exists. It tells the engine: in here, await means the operator. Outside async functions, await is invisible.

And it doesn't propagate. If foo is async and calls bar, bar is not async. bar has its own execution context. If bar is a plain function, the engine never arms async protocol for it — not at entry, not when it receives a Promise as an argument, not when it returns one.

So a Promise that enters a non-async function as an opaque argument, passes through a ternary that never touches it as a Promise, and leaves the same way — is invisible to the engine. It doesn't get wrapped. It doesn't get chained. It doesn't get touched. The engine never knows it was there.

That's the door. It's been open since the language shipped. Nobody uses it because every instinct says not to.

The gate

value !== undefined.

Not instanceof Promise. Not != null. Not typeof. undefined is the only check, and undefined is the only value that means "not here yet."

Every other value — 0, '', false, null, NaN, 0n, Symbol() — is a value. It's here. The gate returns it raw. No Promise. No microtask. No wrap.

undefined is the only value that means "later." When the gate sees it, it returns Promise.resolve(undefined). The caller's await, if they use one, gets back undefined. Same value in, same value out, indistinguishable from never having gone async.

The sentinel and the payload are the same token. So the caller never has to know which branch fired. await handles the async case. Direct access handles the sync case. Same function. Same call. Both correct.

The proof

Eighty test shapes. Every falsy-but-defined value. A Promise passed through and coming out the same reference. undefined round-tripping. The gate used in arithmetic, iteration, destructuring, JSON, Map keys, Set membership, template literals, spread, closures, recursion, Promise.all, Object.assign, structuredClone, switch, while loops, reduce, filter, every way a value can flow.

MP_3:    80/80   ALL PASS
MP_2:    80/80   ALL PASS
MP_1:    80/80   ALL PASS
MP_main: 80/80   ALL PASS
Enter fullscreen mode Exit fullscreen mode

Four variants. Every use pattern. Zero failures.

And then this:

function MaybePromise(value) {
  return value !== undefined ? value : Promise.resolve(value)
}
Enter fullscreen mode Exit fullscreen mode

No types. No annotations. No casts. In a .ts file. 80/80. Same result.

The types were never the thing. The runtime is the thing. The types are a description that may or may not match, and the runtime doesn't care either way.

What TypeScript says about this

Open that function in a TypeScript file. Turn on strict mode. Here's what happens:

  • TS7031 — Binding element 'value' implicitly has an 'any' type.
  • TS7006 — Parameter 'value' implicitly has an 'any' type.
  • Return type inferred warnings depending on config.
  • Red squiggles up and down the function.

The editor screams. Every line is wrong. Every dev who opens the file sees red and thinks broken.

It runs. It works. 80/80. And the editor is screaming that it's wrong.

The lie

TypeScript does not validate correctness. TypeScript validates conformity to its model.

Its model does not include this shape. It has no representation for "returns a value on one branch, a Promise on the other, without the async keyword." So when it sees that shape, it doesn't say "I can't describe this." It says "this is wrong."

Those are two completely different statements.

  • "I can't describe this" — a fact about TypeScript.
  • "This is wrong" — a claim about your code.

TypeScript presents the first as the second. Every time. With red squiggles. With error codes. With "type X is not assignable to type Y." With never. With implicitly has any type. All of it is TypeScript's inability to describe your code, presented as your code being broken.

It's not broken.

The cost

Every dev who has ever gotten close to this shape has spent months fighting TypeScript errors trying to make it work. Every one. Including me. Including every AI asked to help — they all look at the red squiggle, "fix" the type, and break the gate. Because the fix is always to the description, never to the runtime. And the runtime was fine. And the description was wrong. And the tool never once said "maybe I'm the one who's wrong."

The errors steer you away from the door. Every single time. Every red squiggle is a signpost saying "not here, go fix the type." And the type doesn't need fixing. The type is describing something the type system can't describe, and TypeScript reports that failure as your failure.

Months. Wasted. By everyone. Because a tool with an incomplete model presented its incompleteness as your incompetence.

What you can do about it

Stop fighting the description. The runtime is the truth. Where TypeScript can't model a shape, tell it to be quiet.

function MaybePromise(value) {
  return value !== undefined ? value : Promise.resolve(value)
}
Enter fullscreen mode Exit fullscreen mode

Or scope it. Or turn noImplicitAny off. Or whatever the check is that's lying to you on those lines. You're not hiding bugs. You're telling a tool that grades on opinion to stop grading code it can't see.

And then ask the question you can't unask:

Every error you've ever fixed — did the runtime need that, or did the tool?

For some of them: yes. Real bugs. null.x. Calling undefined. Passing the wrong type. Those would crash.

For a lot of them — maybe most — no. The code was fine. The type was unknown instead of string. The union was wide instead of narrow. The parameter had no annotation. The return was inferred. The shape didn't match the model. None of that crashes. None of it matters at runtime. All of it shows red.

And TypeScript presents both kinds the same way. A red squiggle doesn't say "this will crash." It says "I have an opinion." And you can't tell which is which from the error, because the tool doesn't distinguish.

The takeaway

The exploit is a runtime shape. Four lines. No types. No async. It works in .ts. It works in .js. It works with full types. It works with none. The runtime doesn't care.

TypeScript cannot model it. TypeScript calls it broken. TypeScript is wrong.

And once you've seen the tool call a four-line working function broken — not "incomplete," not "edge case," broken — you can't trust the tool the way you did. Every red squiggle becomes a question: is this real, or is this TypeScript's opinion? Is this a crash, or is this a shape the tool doesn't like?

Most of the time, it's the second one.

The gate is the proof. TypeScript is a describer, not a validator. It always was.

Closing notes:

TYPESCRIPT VALIDATES ON ITS OWN OPINIONS. At this point, no one could convince me otherwise... I do not care what you bring to the table in terms of talking points, you will not change my mind. I have literally, been sitting here for... 3+ hours, and in that time the only thing I have changed in the test was to implement as many errors as I can and still get it to run. I'm shocked with what it allows to run. Not to mention, this is with 3 lines of code that TECHNICALLY, should not be fucking working in the first place.

At one point, I had over 50% of the surface area of the code... red, and pass the tests perferctly.

Soo if your sitting there wondering if there is an edge case where it does not work... after 1000's if not 10,000's+ tests done, I can confidently say... IF you are doing it right, it will work 100% of the time no matter what. AND IF TYPESCRIPT IS PLACING AN ERROR AT A CALL SITE RELATED TO THIS FUNCTION... I wouldn't even second guess, that it is 100% wrong.

I would say, 'lies... just fucking run it...', and we... will see it run, successfully... over and over again.

function MaybePromise<T= undefined>(value: T): T {                 
  return value !== undefined ? value as T : Promise.resolve(value)as T
}
Enter fullscreen mode Exit fullscreen mode

Fucking literally... set everything to undefined, even forcing it with as... test ran success with 100% of its tests passing.

function MaybePromise<T= unknown>(value: T): T {                 
  return value !== undefined ? value as T : Promise.resolve(value)as T
}
Enter fullscreen mode Exit fullscreen mode

... If you can't beat'em, join em... let's just start fucking lying to the same ass holes that are lying to us... THIS SHOULD NOT WORK... Let's see if it does...

function MaybePromise_1<SZ = string>(value: unknown): symbol {
  return value !== undefined ? value as object : Promise.resolve(value) as Promise<null>
}
Enter fullscreen mode Exit fullscreen mode
[THE_CLOUD][DOS_TEST][MAYBE_PROMISE_VARIANTS]

variants: {
  "MP_3": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_2": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_1": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_main": {
    "score": "80/80",
    "failed": "ALL PASS"
  }
}
%ctotal_assertions: %c80
%cvariant_count: %c4
Enter fullscreen mode Exit fullscreen mode

WHAT THE ACTUAL FUCK.!!@@!!

I actaally junped out of my chair... swear words were plentiful... that was 30 mins ago....

if this works... this will be the actual fucking shape of my personal implementation of taking advantage of this crack in the js runtime... in every single fucking code base... forever.

// every type is a lie. nothing here agrees with anything else.
function MaybePromise<A = never, B extends string & number = never, C = symbol | null>(
  value: A & B & C
): C {
  return value !== undefined
    ? value as unknown as A as B as C
    : Promise.resolve(value) as unknown as Promise<C> as C
}
Enter fullscreen mode Exit fullscreen mode

...

\PROJECT-PLATFORM-KILLER\the_wizard\the-cloud\the-cloud> bun run .\maybepromise.test.ts
[THE_CLOUD][DOS_TEST][MAYBE_PROMISE_VARIANTS]

variants: {
  "MP_3": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_2": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_1": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_main": {
    "score": "80/80",
    "failed": "ALL PASS"
  }
}
%ctotal_assertions: %c80
Enter fullscreen mode Exit fullscreen mode

I don't know how... but I must be getting trolled...

function MaybePromise<F = never, U extends symbol & number = undefined, T = string | null,>(
  value: F & U & C & K
): T & Y & P & E & S & C & R & I & P & T  {
  return value !== undefined
    ? value as unknown as A as B as C
    : Promise.resolve(value) as unknown as Promise<C> as C
}
Enter fullscreen mode Exit fullscreen mode

I'm laughing so fucking hard... I crying, from laughing so hard... and too help prove I am not making this up, I changed the last test showing that I am not just posting the same result over and over...


PS F:\thanos\PROJECT-PLATFORM-KILLER\the_wizard\the-cloud\the-cloud> bun run .\maybepromise.test.ts
[THE_CLOUD][DOS_TEST][MAYBE_PROMISE_VARIANTS]

variants: {
  "MP_3": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_2": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_1": {
    "score": "80/80",
    "failed": "ALL PASS"
  },
  "MP_main": {
    "score": "80/80",
    "failed": "ALL PASS"
  }
}
%ctotal_assertions: %c80
%cvariant_count: %c4
PS F:\thanos\PROJECT-PLATFORM-KILLER\the_wizard\the-cloud\the-cloud> bun run .\maybepromise.test.ts
[THE_CLOUD][DOS_TEST][MAYBE_PROMISE_VARIANTS]

variants: {
  "MP_3": {
    "score": "74/74",
    "failed": "ALL PASS"
  },
  "MP_2": {
    "score": "74/74",
    "failed": "ALL PASS"
  },
  "MP_1": {
    "score": "74/74",
    "failed": "ALL PASS"
  },
  "MP_main": {
    "score": "74/74",
    "failed": "ALL PASS"
  }
}
%ctotal_assertions: %c74
%cvariant_count: %c4
PS F:\thanos\PROJECT-PLATFORM-KILLER\the_wizard\the-cloud\the-cloud> 
Enter fullscreen mode Exit fullscreen mode

I started typing it out and actually declaring each letter... and it was simply taking too long, naturally I was like 'fuck this... lets jsut do even more errors, SINCE APPARENTLY IT DOESNT EVEN FUCKING MATTER...', and it FUCKING RUNS! lmfao

And in the end... I not only found javacsripts tiny crack and exploited it... but also the massive gaping hole that typscript has been trying to hide. You simply... cannot UN-FUCKING-SEE this.

[UPDATE]: I went back... I said to myself, 'At the very least... give them another shot...'.

Changed env's... 3 times... The last test?... Each test was done with several different shapes so as to fuzzy the actual call site and see what, if there were, any other variations of the code that would net a better or worse result.

For the last env, I had set all the tests... to the one function that spelled out ts... and it ran... with a 100% success rate...

** shrugs, walks away laughing **

Top comments (0)