I have a function say
which works like this:
say()
==> ""
say("hello")()
==> "hello"
say("hello")("how")("are")("you")()
==> "hello how are you"
and is implemented in TypeScript like so:
function say(first?: string) {
if (first === undefined) {
return ""
}
return (second?: string) => {
if (second === undefined) {
return first
}
return say(`${first} ${second}`)
}
}
I left the return type off to see what TypeScript would infer, and it gave me this expression:
function say(first?: string): "" | ((second?: string) => string | ...)
The ...
seems to refer to the fact that any attempt to infer a type is circular, since
say
returns either the empty string or a function that returns either a string or a function that returns either a string or a function that returns either a string or ...
However the use of ...
in the TypeScript rendering of the type does not make clear where the circularity or recursion is, and the type feels inexact. This may be a choice on the part of the TypeScript team. Is there a way to express the type more accurately? Something along the lines of
the function that when given an optional string parameter returns either the empty string or the type t such that t is the type of functions that when given an optional string parameter returns either a string or a t
While TypeScript can internally represent anonymous recursive types, it cannot cannot currently serialize them accurately. Such types are displayed in IntelliSense with the ellipsis "...
", as you've seen, and emitted to declaration files as any
. For example, your say()
function would produce the following declaration:
declare function say(first?: string): "" | ((second?: string) => string | any);
// ^^^
The only way currently to accurately serialize that type would be to give the recursive part a type alias, so it's no longer anonymous and so you can refer to it by name. You can do this manually by inspection (meaning that you have to effectively be your own type checker); your function type is equivalent to
declare function say(first?: string): SayReturn;
type SayReturn = "" | ((second?: string) => string | SayReturn);
which can be confirmed by annotating the return type of say
with SayReturn
and making sure nothing breaks:
type SayReturn = "" | ((second?: string) => string | SayReturn);
function say(first?: string): SayReturn { // okay
if (first === undefined) {
return "" // okay
}
return (second?: string) => {
if (second === undefined) {
return first
}
return say(`${first} ${second}`) // okay
}
}
There is a feature request at microsoft/TypeScript#44045 to have TypeScript automatically synthesize a type alias for such types (presumably with a human-unfriendly generated name like __Type001
). There's also microsoft/TypeScript#30979 which would generate inline type aliases that are locally scoped. And there was also a suggestion at microsoft/TypeScript#517 to support some sort of syntax like ^
for anonymous recursive types. But none of those features are currently part of the language. So for now you'll have to either give up on serialization or do it manually.