I think this is a wrong use of the term "dependent types". For example, you can't write this in TypeScript (yet):
function concat<alen: number, blen: number>(
a: ArrayOfLength<alen>,
b: ArrayOfLength<blen>,
): ArrayOfLength<alen + blen> {
return [...a, ...b];
}
Sure, you can have a type-level object that corresponds to the numbers and use the type-level expression to compute the equivalent of alen + blen, but they are not dependent types. You have just shifted values to the type level. The type-level expression is awesome---but not because they are dependent types.TypeScript is also not a typical type system you can compare with others. Most famously it is intentionally unsound [1], meaning that it can mostly but not entirely prevent a certain class of runtime errors, because it can't guarantee the behavior of external JS code anyway. Most static type systems are or at least try to be sound because that's what users expect them to be: preventing a wrong code beforehand. Dependent type systems are complex because they also want to be sound. TypeScript has an entirely different goal for a reason and you can't compare it with other type systems not sharing that goal.
[1] https://www.typescriptlang.org/docs/handbook/type-compatibil...
Except for some issues regarding the stack size when resolving the types. (But I think there are more efficient implementations using more copy and paste, making this actually work)
playground link:
https://www.typescriptlang.org/play?#code/C4TwDgpgBAggTnAPAO...
Also, probably I should have written my example as follows:
function concat(
alen: number,
blen: number,
a: ArrayOfLength<alen>,
b: ArrayOfLength<blen>,
): ArrayOfLength<alen + blen> {
return [...a, ...b];
}
My original code did distinguish two sets of arguments because that's the TypeScript syntax closest to currying (and I was lazy to check its implication), but the point of dependent typing is that alen and blen are not necessarily known to the compiler. The compiler may have to deal with the following code then: let abc = concat(a, concat(b, c));
abc = concat(concat(a, b), c);
...where the first type `ArrayOfLength<alen + (blen + clen)>` should equal to the second type `ArrayOfLength<(alen + blen) + clen>` for the assignment to be valid. Since the compiler doesn't know alen, blen or clen, this means the compiler needs to somehow understand the associativity! This is hard, considering that (a+b)+c is not even generally same to a+(b+c) thanks to floating point.For this reason many dependently typed languages also act as proof assistants or verifiers. If this point is removed, so-called "const generics" will be classified as dependently typed.
https://www.typescriptlang.org/play?#code/C4TwDgpgBAggTnAPAO...
The operative term here is "a certain amount." `typeof` in Typescript is definitely a dependent type operator (it takes a runtime value and extracts its type which can then be used as a type signature for other types).
You don't have full-spectrum dependent types a la Idris, but you do have some amount of dependent typing. See for example
type MyResult<T extends boolean> =
T extends true ? { tag: 'a' } : { tag: 'b' }
function outputTypeDependsOnInputValue(
value: boolean
): MyResult<typeof value> {
if (value) {
// replacing 'a' with 'b' is a type error
const result: MyResult<typeof value> = { tag: 'a' };
return result;
} else {
// replacing 'b' with 'a' is a type error
const result: MyResult<typeof value> = { tag: 'b' };
return result;
}
}
Now there's a good deal of limitations and hackiness here in how you call this function (in particular you need some weird overloadedness to preserve the types at call sites), but it is really some measure of dependent types! I'll include a full example at the end of this post.> Dependent type systems are complex because they also want to be sound.
Dependent Haskell, if it lands, will not be sound (TypeInType). Idris 2.0 currently also is explicitly not sound. In the case of Idris that's mostly a matter of prioritization, but it's enough to demonstrate that soundness is, perhaps surprisingly, not a huge priority for even all full-spectrum dependently typed languages.
Full TS example:
type MyResult<T extends boolean> =
T extends true ? { tag: 'a' } : { tag: 'b' }
function outputTypeDependsOnInputValue<T extends boolean>(
value: boolean
): MyResult<T>
function outputTypeDependsOnInputValue(
value: boolean
): MyResult<typeof value> {
if (value) {
const result: MyResult<typeof value> = { tag: 'a' };
return result;
} else {
const result: MyResult<typeof value> = { tag: 'b' };
return result;
}
}
function worksWithA(a: 'a'): void {}
function worksWithB(b: 'b'): void {}
function useOurFunction(value: boolean): void {
if (value) {
const result = outputTypeDependsOnInputValue<typeof value>(value);
worksWithA(result.tag);
// worksWithB(result.tag) is a type error
} else {
const result = outputTypeDependsOnInputValue<typeof value>(value);
worksWithB(result.tag);
// worksWithA(result.tag) is a type error
}
}You are absolutely correct that soundness is not the highest priority for dependently typed languages, as it had been true for other languages as well. Still I think most languages with a complex type system (subjective) do try to minimize the number of unsound corners as circumstances permit. I'm not an Idris user by myself but for example, the recent Idris 2 0.3.0 announcement [1] clearly considers unsoundness as something undesirable. As a result I have a general expectation for the type safety of those languages; I don't have that for TypeScript however (and I don't view that as a problem, just a difference).
[1] https://www.idris-lang.org/idris-2-version-030-released.html "Removed multiplicity subtyping, as this is unsound and unfortunately causes more problems than it solves."
It's important to note that what is going on in that TS example isn't just flow typing (otherwise something like Kotlin would count as a dependently typed language). The `typeof` operator is playing a crucial role here. That's what makes this really a case of a type depending on a runtime value.
If it was really just flow typing, then you could still replicate it in something like Scala by just introducing additional layers of types and folding over them (this is also why I would agree flow typing by itself is not dependent types). But that's insufficient to replicate the full TS example.
Idris doesn't want to make its soundness situation any worse than it already it is because the community does eventually want it to be completely sound. But like I said, that's not at the top of its priority list.
EDIT: Removed "Flow-sensitive typing isn't really all that different from dependent pattern matching (the main difference is on the other side of the coin, where you need some way of generating those dependent patterns and of cascading the flow "down" so to speak, so indeed by itself flow-sensitive typing isn't dependent typing). " as the first line because it didn't seem all that relevant in retrospect.
“Brands” allow you to somewhat hack HKTs into Flow/TS, which is used by fp-ts to great effect.
Afaik, they are not available in Typescript and the emulations that I've seen are very sloppy.
Here's an example from the TS handbook: https://www.typescriptlang.org/docs/handbook/unions-and-inte....
What you need to understand about TypeScript is that it ultimately is just JavaScript. After TS 2.0 a decision was made that TS will align with standard EcmaScript and will not implement any* run-time features not part of the ES standard or ES proposals.
Because of this decision TypeScript cannot have a proper dedicated pattern matching syntax until JavaScript gets one. TypeScript does support some pattern matching using ifs and switch statements and other control flow structures, but that's as good as it gets until JS improves as well.
https://github.com/gbegher/groth/blob/460852a71859f149ecf9b5...
Did not cause any roadblocks so far.
Stuff like mapped types, keyOf, typeOf, ReturnType, literal types, conditional types etc. all combine to give you a certain amount of dependent types in Typescript but also a very rich way of doing type level computations (this is how e.g. in Typescript we can have type signatures that capture all sorts of very dynamic Javascript function signatures).
On top of this Typescript also has a strong structural type system augmented by union types and intersection types. You can fairly easily emulate a nominal type system like what Haskell has on top of this system, especially with Typescript's string literal types, but it's quite difficult to go the other way.
The only big thing I can think of that both Haskell and Scala have that Typescript doesn't is higher-kinded types.
I also want to strongly caution that I don't think a more advanced type system is necessarily a better type system! E.g. I'm not convinced that Typescript really needs HKTs.
Likewise I have previously programmed quite happily in Elm despite its paucity of type-level features in comparison to Typescript.