Skip to content

Instantly share code, notes, and snippets.

@pesterhazy
Last active August 12, 2025 11:03
Show Gist options
  • Save pesterhazy/444e51b4b52a06661c8402ff9ee4959c to your computer and use it in GitHub Desktop.
Save pesterhazy/444e51b4b52a06661c8402ff9ee4959c to your computer and use it in GitHub Desktop.
typescript interface unsoundness wat
import test from "node:test";
import assert from "node:assert";
interface IExperiment {
foo(opts: {}): void;
}
class Experiment implements IExperiment {
// Wat?!
// Why is Experiment allowed to implement IExperiment (the typechecker doesn't complain)
// even though IExperiment.foo doesn't guarantee that opts.bar is defined?
foo(opts: { bar: string }): void {
// foo needs opts.bar to be there
assert.ok(opts.bar);
}
}
test.only("experiment", async () => {
const experiment = new Experiment();
experiment.foo({ bar: "baz" });
});
test.only("experiment 2", async () => {
let experiment: IExperiment;
experiment = new Experiment();
experiment.foo({}); // oops, we called foo without passing bar, causing a runtime error
});
/*
How do I change the code so that I can be sure that every class that implements IExperiment
is safe to use with through the interface?
*/
@pesterhazy
Copy link
Author

Ok, so the solution is (and would've never expected this) a subtle change in the interface declaration:

❌ This uses method shorthand syntax and is liable to lead to unsoundness

interface IExperiment {
  foo(opts: {}): void;
}

✅ This uses object property syntax and doesn't allow methods with narrower param types to implement the interface:

interface IExperiment {
  foo = (opts: {}) => void;
}

A subtle change makes a big difference in the type checks!

@pesterhazy
Copy link
Author

Thanks to Matt Pollock for pointing me in the right direction https://bsky.app/profile/mattpocock.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment