Union Types
- Dominant language
- C++
- Stars
- 18.7k
- Forks
- 3.1k
- Avg merge
- 1h 47m
- Merged PRs (30d)
- 2
Description
## Proposal
I would like to propose adding union types into hack, once again.
this request proposes adding the ability to declare a type alias using multiple types, that can be used for both function parameter, and return types.
```hack
type numeric = float|int|Number|BigNumber;
```
## Use case
i have already stated how union types can be used [safely for parameter](https://github.com/facebook/hhvm/issues/7131#issuecomment-475037862), so i decided to also mention a case where i found union types to be really useful, and provides a better DX.
the [Http Controller / Handler](https://github.com/nuxed/framework/blob/rx/src/Nuxed/Http/Server/IHandler.hack#L15) case :
forcing the handler to return an instance of response is not a good DX.
when the response can be just a redirect, the developer would be forced to generate a Uri instance from the route generator, create a response, add the Uri to the response header but after converting it to a string, and create an empty body stream to set as the response body, when ideally they can just return a Uri instance from the generator and the application kernel can take care of the rest, this however would require Nuxed to set the return type to `mixed`, but this is not ideal since `mixed` is too wide ( i.e i don't want the handler to return me a `resource` or an object that i don't know how to deal with. )
using union types, the return type would be :
```hack
namespace Nuxed\Http;
use namespace Nuxed\{Util, Filesystem};
type Response =
Message\Response | // a response
Message\Uri | // redirect
Message\Stream | // body, treat it as a plain text response
Server\IHandler | // another handler to handle the request
string | // plain text
\XHPRoot | // html
Util\Stringable | // plain text
FIlesystem\Node | // download
KeyedContainer; // json objects
```
using the above type, the application kernel, would be able to generate the response and developers would not be forced to construct a response object and set the headers for some responses such html, downloads, json ( default headers can change in the config files ).
however, this is another use case for union types. that won't be solved using method overloading.
## Syntax
Since hack already have type aliases, and using union types in declaration header can get messy, i suggest that union types can only be declared using type aliases as shown above.
```
type arraykey = string|int;
type num = int|float;
type Json = num|string|KeyedContainer|Container|null|bool;
```
## errors, limitations, covariance/contravariance
```hack
<<__Sealed(B::class, C::class)>>
interface A {}
final class B implements A {}
abstract class C implements A {}
class CImpl extends C {}
class D {}
type foo = A|B; // type check error, B already includes A
class E {
const type MyUnion = A|D;
const type RetUnion = B|C;
public function foo(this::MyUnion $union): void {}
public function bar(C $c): void {}
public function baz(): this::RetUnion { return new B(); }
public function qux(): this::RetUnion { return $this->baz(); }
public function lex(): this::RetUnion { return $this->baz(); }
public function herp(): this::RetUnion { return new B(); }
}
class F extends E {
// type error, doesn't accept D anymore, still accepts all A implementations ( note, A is sealed to only B and C )
public function foo(this::RetUnion $un): void {}
public function bar(this::MyUnion $c): void {} // okay, still accepts C
// this would fail in php, but not in hack since A is sealed to only C and B,
// meaning this function can only return C or B, unless, A is not sealed, or allows other
// classes to implement it.
public function baz(): A { return new C(); }
// okay
public function qux(): B { return new B(); }
public function lex(): C {
// type error, parent returns C|B, this function should return only C
return parent::lex();
// okay
return parent::lex() as C;
// okay
$c = parent::lex();
return $c is C ? $c : new CImpl();
}
// type error
public function herp(): D { return new D(); }
}
class G extends E {
// okay, still accepts A and D
public function foo(mixed $un): void {}
}
class H extends E {
// okay, A and D are not null
public function foo(nonnull $un): void {}
}
class Herp extends E {
// error, doesn't accept D anymore
public function foo(A $un): void {}
}
class Berp extends E {
// error, doesn't accept A implementations anymore
public function foo(D $un): void {}
}
class Foo extends E {
const type FooType = B|C|D;
// okay, accepts D and all A implementations ( note : A is sealed to only B and C )
public function foo(this::FooType $un): void {}
}
```
## References :
- https://wiki.php.net/rfc/union_types
- https://en.wikipedia.org/wiki/Union_type
- https://github.com/facebook/hhvm/issues/7131
- https://crystal-lang.org/reference/syntax_and_semantics/union_types.html
- https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types
- https://doc.rust-lang.org/reference/items/unions.html
Contributor guide
Assessment
This issue has not been assessed yet.