I think if you're writing code from scratch, this doesn't really apply -- I'm talking about prototyping language implementations without any libraries at all, sorta like you would do with OCaml from a textbook (e.g. TAPL by Pierce)
(I'm aware of all the terrible experiences people have with TypeScript in the NPM ecosystem. But TypeScript is a big, mature tool and you can use it in more than 1 way.)
I just noticed the 'deno check' command I'm using turns strict mode on by default, so that's good.
All widely used gradual type systems are unsound because they have to interoperate with untyped code, and the dynamic checks to make it sound are too expensive.
But code written from scratch doesn't have that issue. I'd be interested in a counterexample -- is there a code snippet that passes the strict mode of the compiler, and doesn't interoperate with untyped code, but produces an unexpected runtime error?
I guess by "unexpected" I mean that, at runtime, an operation is performed on a value which is not allowed, and the program fails
---
I googled and found this -- https://effectivetypescript.com/2021/05/06/unsoundness/ -- not sure I agree with some points, e.g. array out of bounds isn't unsoundness! The OPERATION is legal, but the data isn't, which isn't something that any type system will tell you.
Similar to divide by zero -- a runtime error does not imply unsoundness.
Also, casts can produce unexpected runtime errors by definition -- that's why they are casts, and you have to opt in! Bad article.
> But code written from scratch doesn't have that issue. I'd be interested in a counterexample -- is there a code snippet that passes the strict mode of the compiler, and doesn't interoperate with untyped code, but produces an unexpected runtime error?
You'd think so, right? But no, typescript is deliberately unsound in ways that have nothing to do with gradual typing. Here are a few examples.
Signatures written in method syntax are bivariant, which is not correct
interface Unsound {
f(x: number | string): number
}
interface Unsound2 {
f(x: number): number
}
const a: Unsound2 = { f: (x: number) => x }
const b: Unsound = a
const c: number = b.f("not a number")
Type predicate results survive mutation
const hasA = (x: object): x is { a: unknown } => "a" in x
const deleteA = (x: { a: unknown }) => {
delete x.a
}
const unsound = (x: object) => {
if (hasA(x)) {
deleteA(x)
return x.a
} else {
return "no a"
}
}
Many stdlib types are incorrect. JSON stuff is particularly bad: JSON.parse and Body.json() both return `any`.
You can spread things that aren't objects
const unsound = <X,Y>(x: X, y: Y): X & Y => ({...x, ...y})
const bad: never = unsound(5, 4)
(And even for objects, `X & Y` is not the correct type when you have overlapping keys)
Anything with optional fields can be widened incorrectly
const unsound = <T extends { x: number }>(t: T): { x: number, y?: number } => t
const bad: number | undefined = unsound({ x: 5, y: "not a number" }).y
Assignment doesn't handle `readonly` properly
interface Readonly {
readonly x: number
}
interface Mutable {
x: number
}
const a: Readonly = Object.freeze({x: 5 })
const b: Mutable = a
b.x = 4
> The OPERATION is legal, but the data isn't, which isn't something that any type system will tell you.
There are some that will, though unfortunately none that are really production-ready yet.
Great examples, thanks!! I typed them all into the TypeScript playground.
I agree this is weird, and seems to follow from TypeScript's heritage as "trying to describe whatever dynamic JS does"
I mean that's probably why I didn't use it for >10 years (in addition to its JS heritage). But I did find that there is an interesting subset, at least for playing around.
I think the JSON.parse() issue is fundamental -- it's not clear what they could have done better, and static languages don't really do better. There is a fundamental problem there -- type systems are interior to a process, while data is exterior (https://www.oilshell.org/blog/2023/06/ysh-design.html)
> I think the JSON.parse() issue is fundamental -- it's not clear what they could have done better, and static languages don't really do better.
The best solution, IMO, is to give up on "no type-directed emit" (which harms the language in lots of other ways as well) and derive appropriate parsers at compile-time. Parsing malformed data should fail immediately, not just when you try to use the broken parts. This is a solved problem in C#, C++, Haskell, and no doubt many other languages.
Failing that, it should return an appropriate `JSON` type. Something along the lines of
type Field = string | number | boolean | null | JSON
type JSON = {[key in string]?: Field } | Field[]
(I'm aware of all the terrible experiences people have with TypeScript in the NPM ecosystem. But TypeScript is a big, mature tool and you can use it in more than 1 way.)
I just noticed the 'deno check' command I'm using turns strict mode on by default, so that's good.
https://deno.land/manual@v1.4.1/getting_started/typescript
All widely used gradual type systems are unsound because they have to interoperate with untyped code, and the dynamic checks to make it sound are too expensive.
But code written from scratch doesn't have that issue. I'd be interested in a counterexample -- is there a code snippet that passes the strict mode of the compiler, and doesn't interoperate with untyped code, but produces an unexpected runtime error?
I guess by "unexpected" I mean that, at runtime, an operation is performed on a value which is not allowed, and the program fails
---
I googled and found this -- https://effectivetypescript.com/2021/05/06/unsoundness/ -- not sure I agree with some points, e.g. array out of bounds isn't unsoundness! The OPERATION is legal, but the data isn't, which isn't something that any type system will tell you.
Similar to divide by zero -- a runtime error does not imply unsoundness.
Also, casts can produce unexpected runtime errors by definition -- that's why they are casts, and you have to opt in! Bad article.
---
I think these are better examples: https://news.ycombinator.com/item?id=15659657
I believe Java has some of those too. Covariance / contravariance is a common source of unsoundness, but definitely not a dealbreaker for me