generic types - syntax comparison
See original GitHub issueOverview and comparison of generic type syntax proposals
This issue lists and compares several syntax proposals that have been proposed and initially discussed in issue #55. It should help to come to a decision which syntax proposal should be used.
Note: The following aspects have not been discussed yet and are not part of this overview/comparison:
- variance (covariant, contravariant and invariant), see issue #81
- constraints on generic type arguments, see issue #82
Table of Contents
- Examples
- Syntax proposals
- Comparison of syntax proposals
Examples
It might be easier for you to reason about syntax if you see some examples. Therefore, I will describe three function types Transform
, Filter
and Map
and an interface type Collection
in different syntaxes. I provide different writing styles of each syntax. For example, you might write Filter
or Map
with use of Transform
or using an inline type expression instead.
Functions
In this section I describe the signature of the three generic function types. Further, I describe each type argument and give an example of an function implementation in JavaScript with its type as a comment.
Transform
Name: Transform
Type arguments: Input, Output
Signature: Transform(Input): Output
Description: A function that takes a value of type Input
and returns a value of type Output
.
Example: The function len
takes a String
as parameter and returns its length as type Number
. Thus it is of type Transform
where Input
is String
and Output
is Number
.
// Transform(String): Number
function len(str) {
return str.length;
}
Filter
Name: Filter
Type arguments: Element
Signature: Filter(Element => Boolean, Element[]): Element[]
Description: Filter elements of type Element
in an array. Take a function of type Element => Boolean
(is equivalent to Transfrom(Element): Boolean
) as first parameter. Takes array of type Element[]
as second parameter. Returns an array of type Element[]
.
Example: An generic implementation might look like this:
// Filter(Element => Boolean, Element[]): Element[]
function filter(predicate, elements) {
return elements.filter(predicate);
}
The generic type argument Element
has to be inferred from the passed arguments of the filter
call.
// Transform(Number): Boolean
function isEven(x) {
return x % 2 === 0;
}
// Transform(String): Boolean
function isShort(str) {
return str.length < 4;
}
// Number[]
var numbers = [1, 2, 3, 4];
// String[]
var strings = ["rtype", "is", "cool"];
// Filter(Number => Boolean, Number[]): Number[]
filter(isEven, numbers); // [2, 4]: Number[]
// Filter(String => Boolean, String[]): String[]
filter(isShort, strings); // ["is"]: String[]
// type conflicts if incompatible types are use as type argument `Element`
filter(isEven, strings); // Error: In "Filter" the first type argument "Element" couldn't match both (incompatible) types Number and String
filter(isShort, numbers); // Error: In "Filter" the first type argument "Element" couldn't match both (incompatible) types String and Number
Map
Name: Map
Type arguments: Input, Output
Signature: Map(Input => Output, Input[]): Output[]
Description: Filter elements of type Element
in an array. Take a function of type Element => Boolean
(is equivalent to Transfrom(Element): Boolean
) as first parameter. Takes array of type Element[]
as second parameter. Returns an array of type Element[]
.
Example: Map elements of type Input
to type Output
. Take a function of type Input => Output
(is equivalent to Transfrom(Input): Output
) as first parameter. Takes array of type Input[]
as second parameter. Returns an array of type Output[]
.
// Map(Input => Output, Input[]): Output[]
function map(transform, elements) {
return elements.map(transform);
}
The generic type arguments Input
and Output
have to be inferred from the passed arguments of the map
call.
// Transform(Number): Number
function square(x) {
return x * x;
}
// Transform(String): Number
function len(str) {
return str.length;
}
// Number[]
var numbers = [1, 2, 3, 4];
// String[]
var strings = ["rtype", "is", "cool"];
// Map(Number => Number, Number[]): Number[]
map(square, numbers); // [1, 4, 9, 16]: Number[]
// Map(String => Number, String[]): Number[]
map(len, strings); // [5, 2, 4]: Number[]
map(square, strings); // Error: In "Map" the first type argument "Input" couldn't match both (incompatible) types Number and String
map(len, numbers); // Error: In "Map" the first type argument "Input" couldn't match both (incompatible) types String and Number
Interface
To keep it simple, I describe an interface Collection
that is satisfied by most common data structure objects (for example Array).
Name: Collection
Type arguments: Element
Signature:
// takes a type argument Element
interface Collection {
filter(Element => Boolean): Collection // with elements of type `Element`
// map is generic and takes an additional type argument `NewElement`
map(Element => NewElement): Collection // with elements of type `NewElement`
}
Description: The interface Collection
takes one type argument Element
that is visible in the interface body. All elements in the data structure are of this type Element
. A Collection
has methods filter
and map
. They do the same as the functions filter
and map
I described in the chapter above, but since they are methods, the data structure is (implicitly) passed as this
argument. Hence, the last argument of the corresponding function is dropped in the method.
Example: Let’s see some JavaScript code with Array
as Collection
to see the difference between the function filter
and the method filter
// Number => Boolean
function isEven(x) {
return x % 2 === 0;
}
// Number[]
// Satisfies Collection where type argument Element is Number
var numbers = [1, 2, 3, 4];
// Filter(Number => Boolean, Number[]): Number[]
filter(isEven, numbers); // [2, 4]: Number[]
// numbers: Collection where type argument Element is Number
// numbers.filter: (Number => Boolean): Number[]
numbers.filter(isEven); // [2, 4]: Number[] which satisfies Collection where type argument Element is Number
and the difference between the function map
and the method map
:
// Transform(String): Number
function len(str) {
return str.length;
}
// String[]
// Satisfies Collection where type argument Element is String
var strings = ["rtype", "is", "cool"];
// Map(String => Number, String[]): Number[]
map(len, strings); // [5, 2, 4]: Number[]
// numbers: Collection where type argument Element is String
// numbers.map: (String => Number): Number[]
strings.map(len); // [5, 2, 4]: Number[] which satisfies Collection where type argument Element is Number
Syntax proposals
Listed in the subchapters below are type declarations of the example types Transform
, Filter
, Map
and Collection
written in different variants of each syntax proposal. In all syntax proposals the name of the type is followed by its type arguments followed by its type definition. The difference is how type arguments are written. Are they enclosed in angle or square brackets, parentheses or are they only separated by whitespace. The different variants of a syntax vary in several aspects that might influence readability:
- with or without colons after type signature
- multi- vs. single-line
- (some) type expressions enclosed in parentheses:
Input => Output
vs.(Input) => Output
vs.(Input => Output)
used in a specific context - additional type argument declaration before or after the method name (here
map
)
Angle Brackets Syntax
Type arguments are enclosed in angle brackets and are separated by commas. This syntax is influenced by C++, Java, C# and TypeScript.
single-line without colons
Functions
Transform<Input, Output> Input => Output
Transform<Input, Output> (Input) => Output
Transform<Input, Output> (Input => Output)
Filter<Element> (Element => Boolean, Element[]) => Element[]
Filter<Element> (Transform<Element, Boolean>, Element[]) => Element[]
Map<Input, Output> (Input => Output, Input[]) => Output[]
Map<Input, Output> (Transform<Input, Output>, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
filter(Element => Boolean) => Collection<Element>
filter(Transform<Element, Boolean>) => Collection<Element>
<NewElement> map(transform: Element => NewElement) => Collection<NewElement>
<NewElement> map(transform: Transform<Element, NewElement>) => Collection<NewElement>
<NewElement> map(Transform<Element, NewElement>) => Collection<NewElement>
}
Generic type arguments of methods after method name
interface Collection<Element> {
filter(Element => Boolean) => Collection<Element>
filter(Transform<Element, Boolean>) => Collection<Element>
map<NewElement>(transform: Element => NewElement) => Collection<NewElement>
map<NewElement>(transform: Transform<Element, NewElement>) => Collection<NewElement>
map<NewElement>(Transform<Element, NewElement>) => Collection<NewElement>
}
single-line with colons
Functions
Transform<Input, Output>: Input => Output
Transform<Input, Output>: (Input) => Output
Transform<Input, Output>: (Input => Output)
Filter<Element>: (Element => Boolean, Element[]) => Element[]
Filter<Element>: (Transform<Element, Boolean>, Element[]) => Element[]
Map<Input, Output>: (Input => Output, Input[]) => Output[]
Map<Input, Output>: (Transform<Input, Output>, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
filter: (Element => Boolean) => Collection<Element>
filter: (Transform<Element, Boolean>) => Collection<Element>
<NewElement> map: (transform: Element => NewElement) => Collection<NewElement>
<NewElement> map: (transform: Transform<Element, NewElement>) => Collection<NewElement>
<NewElement> map: (Transform<Element, NewElement>) => Collection<NewElement>
}
Generic type arguments of methods after method name
interface Collection<Element> {
filter: (Element => Boolean) => Collection<Element>
filter: (Transform<Element, Boolean>) => Collection<Element>
map<NewElement>: (transform: Element => NewElement) => Collection<NewElement>
map<NewElement>: (transform: Transform<Element, NewElement>) => Collection<NewElement>
map<NewElement>: (Transform<Element, NewElement>) => Collection<NewElement>
}
multiline with colons
Functions
Transform<Input, Output>:
Input => Output
Transform<Input, Output>:
(Input) => Output
Transform<Input, Output>:
(Input => Output)
Filter<Element>:
(Element => Boolean, Element[]) => Element[]
Filter<Element>:
(Transform<Element, Boolean>, Element[]) => Element[]
Map<Input, Output>:
(Input => Output, Input[]) => Output[]
Map<Input, Output>:
(Transform<Input, Output>, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
filter:
(Element => Boolean) => Collection<Element>
filter:
(Transform<Element, Boolean>) => Collection<Element>
<NewElement> map:
(transform: Element => NewElement) => Collection<NewElement>
<NewElement> map:
(transform: Transform<Element, NewElement>) => Collection<NewElement>
<NewElement> map:
(Transform<Element, NewElement>) => Collection<NewElement>
}
Generic type arguments of methods after method name
interface Collection<Element> {
filter:
(Element => Boolean) => Collection<Element>
filter:
(Transform<Element, Boolean>) => Collection<Element>
map<NewElement>:
(transform: Element => NewElement) => Collection<NewElement>
map<NewElement>:
(transform: Transform<Element, NewElement>) => Collection<NewElement>
map<NewElement>:
(Transform<Element, NewElement>) => Collection<NewElement>
}
multiline with curly brackets
Functions
Transform<Input, Output> {
Input => Output
}
Transform<Input, Output> {
(Input) => Output
}
Transform<Input, Output> {
(Input => Output)
}
Filter<Element> {
(Element => Boolean, Element[]) => Element[]
}
Filter<Element> {
(Transform<Element, Boolean>, Element[]) => Element[]
}
Map<Input, Output> {
(Input => Output, Input[]) => Output[]
}
Map<Input, Output> {
(Transform<Input, Output>, Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection<Element> {
filter {
(Element => Boolean) => Collection<Element>
}
filter {
(Transform<Element, Boolean>) => Collection<Element>
}
<NewElement> map {
(transform: Element => NewElement) => Collection<NewElement>
}
<NewElement> map {
(transform: Transform<Element, NewElement>) => Collection<NewElement>
}
<NewElement> map {
(Transform<Element, NewElement>) => Collection<NewElement>
}
}
Generic type arguments of methods after method name
interface Collection<Element> {
filter {
(Element => Boolean) => Collection<Element>
}
filter {
(Transform<Element, Boolean>) => Collection<Element>
}
map<NewElement> {
(transform: Element => NewElement) => Collection<NewElement>
}
map<NewElement> {
(transform: Transform<Element, NewElement>) => Collection<NewElement>
}
map<NewElement> {
(Transform<Element, NewElement>) => Collection<NewElement>
}
}
Square Brackets Syntax
Type arguments are enclosed in square brackets and are separated by commas. This syntax is influenced by Scala.
single-line without colons
Functions
Transform[Input, Output] Input => Output
Transform[Input, Output] (Input) => Output
Transform[Input, Output] (Input => Output)
Filter[Element] (Element => Boolean, Element[]) => Element[]
Filter[Element] (Transform[Element, Boolean], Element[]) => Element[]
Map[Input, Output] (Input => Output, Input[]) => Output[]
Map[Input, Output] (Transform[Input, Output], Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
filter(Element => Boolean) => Collection[Element]
filter(Transform[Element, Boolean]) => Collection[Element]
[NewElement] map(transform: Element => NewElement) => Collection[NewElement]
[NewElement] map(transform: Transform[Element, NewElement]) => Collection[NewElement]
[NewElement] map(Transform[Element, NewElement]) => Collection[NewElement]
}
Generic type arguments of methods after method name
interface Collection[Element] {
filter(Element => Boolean) => Collection[Element]
filter(Transform[Element, Boolean]) => Collection[Element]
map[NewElement](transform: Element => NewElement) => Collection[NewElement]
map[NewElement](transform: Transform[Element, NewElement]) => Collection[NewElement]
map[NewElement](Transform[Element, NewElement]) => Collection[NewElement]
}
single-line with colons
Functions
Transform[Input, Output]: Input => Output
Transform[Input, Output]: (Input) => Output
Transform[Input, Output]: (Input => Output)
Filter[Element]: (Element => Boolean, Element[]) => Element[]
Filter[Element]: (Transform[Element, Boolean], Element[]) => Element[]
Map[Input, Output]: (Input => Output, Input[]) => Output[]
Map[Input, Output]: (Transform[Input, Output], Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
filter: (Element => Boolean) => Collection[Element]
filter: (Transform[Element, Boolean]) => Collection[Element]
[NewElement] map: (transform: Element => NewElement) => Collection[NewElement]
[NewElement] map: (transform: Transform[Element, NewElement]) => Collection[NewElement]
[NewElement] map: (Transform[Element, NewElement]) => Collection[NewElement]
}
Generic type arguments of methods after method name
interface Collection[Element] {
filter: (Element => Boolean) => Collection[Element]
filter: (Transform[Element, Boolean]) => Collection[Element]
map[NewElement]: (transform: Element => NewElement) => Collection[NewElement]
map[NewElement]: (transform: Transform[Element, NewElement]) => Collection[NewElement]
map[NewElement]: (Transform[Element, NewElement]) => Collection[NewElement]
}
multiline with colons
Functions
Transform[Input, Output]:
Input => Output
Transform[Input, Output]:
(Input) => Output
Transform[Input, Output]:
(Input => Output)
Filter[Element]:
(Element => Boolean, Element[]) => Element[]
Filter[Element]:
(Transform[Element, Boolean], Element[]) => Element[]
Map[Input, Output]:
(Input => Output, Input[]) => Output[]
Map[Input, Output]:
(Transform[Input, Output], Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
filter:
(Element => Boolean) => Collection[Element]
filter:
(Transform[Element, Boolean]) => Collection[Element]
[NewElement] map:
(transform: Element => NewElement) => Collection[NewElement]
[NewElement] map:
(transform: Transform[Element, NewElement]) => Collection[NewElement]
[NewElement] map:
(Transform[Element, NewElement]) => Collection[NewElement]
}
Generic type arguments of methods after method name
interface Collection[Element] {
filter:
(Element => Boolean) => Collection[Element]
filter:
(Transform[Element, Boolean]) => Collection[Element]
map[NewElement]:
(transform: Element => NewElement) => Collection[NewElement]
map[NewElement]:
(transform: Transform[Element, NewElement]) => Collection[NewElement]
map[NewElement]:
(Transform[Element, NewElement]) => Collection[NewElement]
}
multiline with curly brackets
Functions
Transform[Input, Output] {
Input => Output
}
Transform[Input, Output] {
(Input) => Output
}
Transform[Input, Output] {
(Input => Output)
}
Filter[Element] {
(Element => Boolean, Element[]) => Element[]
}
Filter[Element] {
(Transform[Element, Boolean], Element[]) => Element[]
}
Map[Input, Output] {
(Input => Output, Input[]) => Output[]
}
Map[Input, Output] {
(Transform[Input, Output], Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection[Element] {
filter {
(Element => Boolean) => Collection[Element]
}
filter {
(Transform[Element, Boolean]) => Collection[Element]
}
[NewElement] map {
(transform: Element => NewElement) => Collection[NewElement]
}
[NewElement] map {
(transform: Transform[Element, NewElement]) => Collection[NewElement]
}
[NewElement] map {
(Transform[Element, NewElement]) => Collection[NewElement]
}
}
Generic type arguments of methods after method name
interface Collection[Element] {
filter {
(Element => Boolean) => Collection[Element]
}
filter {
(Transform[Element, Boolean]) => Collection[Element]
}
map[NewElement] {
(transform: Element => NewElement) => Collection[NewElement]
}
map[NewElement] {
(transform: Transform[Element, NewElement]) => Collection[NewElement]
}
map[NewElement] {
(Transform[Element, NewElement]) => Collection[NewElement]
}
}
Parentheses Syntax
Type arguments are enclosed in parentheses and are separated by commas. This syntax is influenced by JavaScript function notation. Since a generic type is a type constructor/function that takes types as arguments and returns a concrete type.
single-line without colons
Functions
Transform(Input, Output) Input => Output
Transform(Input, Output) (Input) => Output
Transform(Input, Output) (Input => Output)
Filter(Element) (Element => Boolean, Element[]) => Element[]
Filter(Element) (Transform(Element, Boolean), Element[]) => Element[]
Map(Input, Output) (Input => Output, Input[]) => Output[]
Map(Input, Output) (Transform(Input, Output), Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
filter(Element => Boolean) => Collection(Element)
filter(Transform(Element, Boolean)) => Collection(Element)
(NewElement) map(transform: Element => NewElement) => Collection(NewElement)
(NewElement) map(transform: Transform(Element, NewElement)) => Collection(NewElement)
(NewElement) map(Transform(Element, NewElement)) => Collection(NewElement)
}
Generic type arguments of methods after method name
interface Collection(Element) {
filter(Element => Boolean) => Collection(Element)
filter(Transform(Element, Boolean)) => Collection(Element)
map(NewElement)(transform: Element => NewElement) => Collection(NewElement)
map(NewElement)(transform: Transform(Element, NewElement)) => Collection(NewElement)
map(NewElement)(Transform(Element, NewElement)) => Collection(NewElement)
}
single-line with colons
Functions
Transform(Input, Output): Input => Output
Transform(Input, Output): (Input) => Output
Transform(Input, Output): (Input => Output)
Filter(Element): (Element => Boolean, Element[]) => Element[]
Filter(Element): (Transform(Element, Boolean), Element[]) => Element[]
Map(Input, Output): (Input => Output, Input[]) => Output[]
Map(Input, Output): (Transform(Input, Output), Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
filter: (Element => Boolean) => Collection(Element)
filter: (Transform(Element, Boolean)) => Collection(Element)
(NewElement) map: (transform: Element => NewElement) => Collection(NewElement)
(NewElement) map: (transform: Transform(Element, NewElement)) => Collection(NewElement)
(NewElement) map: (Transform(Element, NewElement)) => Collection(NewElement)
}
Generic type arguments of methods after method name
interface Collection(Element) {
filter: (Element => Boolean) => Collection(Element)
filter: (Transform(Element, Boolean)) => Collection(Element)
map(NewElement): (transform: Element => NewElement) => Collection(NewElement)
map(NewElement): (transform: Transform(Element, NewElement)) => Collection(NewElement)
map(NewElement): (Transform(Element, NewElement)) => Collection(NewElement)
}
multiline with colons
Functions
Transform(Input, Output):
Input => Output
Transform(Input, Output):
(Input) => Output
Transform(Input, Output):
(Input => Output)
Filter(Element):
(Element => Boolean, Element[]) => Element[]
Filter(Element):
(Transform(Element, Boolean), Element[]) => Element[]
Map(Input, Output):
(Input => Output, Input[]) => Output[]
Map(Input, Output):
(Transform(Input, Output), Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
filter:
(Element => Boolean) => Collection(Element)
filter:
(Transform(Element, Boolean)) => Collection(Element)
(NewElement) map:
(transform: Element => NewElement) => Collection(NewElement)
(NewElement) map:
(transform: Transform(Element, NewElement)) => Collection(NewElement)
(NewElement) map:
(Transform(Element, NewElement)) => Collection(NewElement)
}
Generic type arguments of methods after method name
interface Collection(Element) {
filter:
(Element => Boolean) => Collection(Element)
filter:
(Transform(Element, Boolean)) => Collection(Element)
map(NewElement):
(transform: Element => NewElement) => Collection(NewElement)
map(NewElement):
(transform: Transform(Element, NewElement)) => Collection(NewElement)
map(NewElement):
(Transform(Element, NewElement)) => Collection(NewElement)
}
multiline with curly brackets
Functions
Transform(Input, Output) {
Input => Output
}
Transform(Input, Output) {
(Input) => Output
}
Transform(Input, Output) {
(Input => Output)
}
Filter(Element) {
(Element => Boolean, Element[]) => Element[]
}
Filter(Element) {
(Transform(Element, Boolean), Element[]) => Element[]
}
Map(Input, Output) {
(Input => Output, Input[]) => Output[]
}
Map(Input, Output) {
(Transform(Input, Output), Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection(Element) {
filter {
(Element => Boolean) => Collection(Element)
}
filter {
(Transform(Element, Boolean)) => Collection(Element)
}
(NewElement) map {
(transform: Element => NewElement) => Collection(NewElement)
}
(NewElement) map {
(transform: Transform(Element, NewElement)) => Collection(NewElement)
}
(NewElement) map {
(Transform(Element, NewElement)) => Collection(NewElement)
}
}
Generic type arguments of methods after method name
interface Collection(Element) {
filter {
(Element => Boolean) => Collection(Element)
}
filter {
(Transform(Element, Boolean)) => Collection(Element)
}
map(NewElement) {
(transform: Element => NewElement) => Collection(NewElement)
}
map(NewElement) {
(transform: Transform(Element, NewElement)) => Collection(NewElement)
}
map(NewElement) {
(Transform(Element, NewElement)) => Collection(NewElement)
}
}
Whitespace Syntax
Type arguments are written without backets and are separated by whitespace. This syntax is influenced by Elm and Haskell.
single-line without colons
Functions
Transform Input Output Input => Output
Transform Input Output (Input) => Output
Transform Input Output (Input => Output)
Filter Element (Element => Boolean, Element[]) => Element[]
Filter Element (Transform Element Boolean, Element[]) => Element[]
Map Input Output (Input => Output, Input[]) => Output[]
Map Input Output (Transform Input Output, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection Element {
filter(Element => Boolean) => Collection Element
filter(Transform Element Boolean) => Collection Element
NewElement map(transform: Element => NewElement) => Collection NewElement
NewElement map(transform: Transform Element NewElement) => Collection NewElement
NewElement map(Transform Element NewElement) => Collection NewElement
}
Generic type arguments of methods after method name
interface Collection Element {
filter(Element => Boolean) => Collection Element
filter(Transform Element Boolean) => Collection Element
map NewElement(transform: Element => NewElement) => Collection NewElement
map NewElement(transform: Transform Element NewElement) => Collection NewElement
map NewElement(Transform Element NewElement) => Collection NewElement
}
single-line with colons
Functions
Transform Input Output: Input => Output
Transform Input Output: (Input) => Output
Transform Input Output: (Input => Output)
Filter Element: (Element => Boolean, Element[]) => Element[]
Filter Element: (Transform Element Boolean, Element[]) => Element[]
Map Input Output: (Input => Output, Input[]) => Output[]
Map Input Output: (Transform Input Output, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection Element {
filter: (Element => Boolean) => Collection Element
filter: (Transform Element Boolean) => Collection Element
NewElement map: (transform: Element => NewElement) => Collection NewElement
NewElement map: (transform: Transform Element NewElement) => Collection NewElement
NewElement map: (Transform Element NewElement) => Collection NewElement
}
Generic type arguments of methods after method name
interface Collection Element {
filter: (Element => Boolean) => Collection Element
filter: (Transform Element Boolean) => Collection Element
map NewElement: (transform: Element => NewElement) => Collection NewElement
map NewElement: (transform: Transform Element NewElement) => Collection NewElement
map NewElement: (Transform Element NewElement) => Collection NewElement
}
multiline with colons
Functions
Transform Input Output:
Input => Output
Transform Input Output:
(Input) => Output
Transform Input Output:
(Input => Output)
Filter Element:
(Element => Boolean, Element[]) => Element[]
Filter Element:
(Transform Element Boolean, Element[]) => Element[]
Map Input Output:
(Input => Output, Input[]) => Output[]
Map Input Output:
(Transform Input Output, Input[]) => Output[]
Interface
Generic type arguments of methods before method name
interface Collection Element {
filter:
(Element => Boolean) => Collection Element
filter:
(Transform Element Boolean) => Collection Element
NewElement map:
(transform: Element => NewElement) => Collection NewElement
NewElement map:
(transform: Transform Element NewElement) => Collection NewElement
NewElement map:
(Transform Element NewElement) => Collection NewElement
}
Generic type arguments of methods after method name
interface Collection Element {
filter:
(Element => Boolean) => Collection Element
filter:
(Transform Element Boolean) => Collection Element
map NewElement:
(transform: Element => NewElement) => Collection NewElement
map NewElement:
(transform: Transform Element NewElement) => Collection NewElement
map NewElement:
(Transform Element NewElement) => Collection NewElement
}
multiline with curly brackets
Functions
Transform Input Output {
Input => Output
}
Transform Input Output {
(Input) => Output
}
Transform Input Output {
(Input => Output)
}
Filter Element {
(Element => Boolean, Element[]) => Element[]
}
Filter Element {
(Transform Element Boolean, Element[]) => Element[]
}
Map Input Output {
(Input => Output, Input[]) => Output[]
}
Map Input Output {
(Transform Input Output, Input[]) => Output[]
}
Interface
Generic type arguments of methods before method name
interface Collection Element {
filter {
(Element => Boolean) => Collection Element
}
filter {
(Transform Element Boolean) => Collection Element
}
NewElement map {
(transform: Element => NewElement) => Collection NewElement
}
NewElement map {
(transform: Transform Element NewElement) => Collection NewElement
}
NewElement map {
(Transform Element NewElement) => Collection NewElement
}
}
Generic type arguments of methods after method name
interface Collection Element {
filter {
(Element => Boolean) => Collection Element
}
filter {
(Transform Element Boolean) => Collection Element
}
map NewElement {
(transform: Element => NewElement) => Collection NewElement
}
map NewElement {
(transform: Transform Element NewElement) => Collection NewElement
}
map NewElement {
(Transform Element NewElement) => Collection NewElement
}
}
Comparison of syntax proposals
It is hard to compare syntax impersonal and objectively. Opinions might differ on readability and the question if familiarity of a syntax is an advantage or an disadvantage. Thus, I mainly collect opinions as quotations. I will add opinions that arise in further discussion. Please tell me if any opinion or aspect is missing or if something is not quite appropriate.
Angle Brackets Syntax
Similar to C++ templates and Java, C# and TypeScript generics.
Thousands of C++/Java/C# developers are currently being converted to JS developers, you all know that. Students in (high)schools study the C++/Java/C#. I would recommend to make sure these people can read rtype easily without browsing the documentation (JSDoc is quite readable, btw, thus popular). […] TypeScript syntax is readable to most developers – koresar
The problem is that the <> syntax isn’t consistent between languages anyway. Look at Java, C#, Kotlin, Rust, etc. all of them are doing it differently. – unknown reddit user
TypeScript’s generics in function signatures, and the TypeScript syntax feels like an assault on my senses – ericelliott
I think there’s something about the angle brackets that drives me crazy. Reminds me of C++ templates or something (which I have bad memories of). – ericelliott
👎 For angle brackets – BerkeleyTrue
Java’s approach is saying “f*ck it”, let’s just have stuff like
method<String>()
, butinstance.<String>method()
at use-site; and at declaration-site (different issue) it’s even more different withclass Box<T>
vs.<T> void foo(T t)
. – unknown reddit user
Advantages
- common syntax in many languages
Disadvatages
-
< (Unicode Character ‘LESS-THAN SIGN’) and > (Unicode Character ‘GREATER-THAN SIGN’) used as brackets are harder to read, since they are no real brackets. The real angle brackets are 〈 (Unicode Character ‘LEFT ANGLE BRACKET’) and 〉 (Unicode Character ‘RIGHT ANGLE BRACKET’).
-
parsing issues
Using
<>
for generics is a historical accident. If you look at all the languages which adopted it, all of them need horrible hacks to work around the intrinsic issues. – unknown reddit userDo you have any examples where they had to work around these parsing issues due to using <> for generics?
C# and Kotlin for example just keep reading after the
<
and have a huge list of tokens to determine whether it’s a comparison, Generics, or if they need to keep reading further. They basically need unlimited lookahead, and if they figure it out, they rollback the parser state and parse it again, with more information. – unknown reddit userIn return you avoid the compiler having to look ahead a little during parsing. Is that really a good trade? I don’t.
If even the parser has trouble reading, people will very likely also have trouble with comprehending things. – unknown reddit user
In case of a single-parameter-function as concrete type argument readability is decreased with angle brackets notation due to the fact that a closing angle bracket is used as arrow head:
MyInterface<String => String> // angle bracket syntax MyInterface<(String => String)> // angle bracket syntax, IMO parentheses don't increase readability in this case MyInterface (String => String) // whitespace syntax (type argument has to be enclosed in parentheses, otherwise it would be equal to `(MyInterface String) => String` MyInterface(String => String) // parentheses/function syntax; Note: Could also be written like whitespace syntax in this case: `MyInterface (String => String)`
– maiermic
map<t, u>(fn: (x: t) => u, list: t[]) => u[];
IMO, this is even worse than the C++ & Java versions because of the arrow functions. Note how the closing angle bracket for the type parameter declaration looks visually very similar to the arrows in the arrow functions. My brain tries to match them up.My brain also struggles with the groupings of type parameters and function parameters, mostly lost in the syntax noise.
[…]
interface StringStringThing: MyInterface<String => String>
Which visually looks a bit like this:
interface StringStringThing: MyInterface<String => // wtf is the rest of that nonsense?
Square Brackets Syntax
Similar to Scala generics
Java’s approach is saying “f*ck it”, let’s just have stuff like
method<String>()
, butinstance.<String>method()
at use-site; and at declaration-site (different issue) it’s even more different withclass Box<T>
vs.<T> void foo(T t)
. In Scala, everything is regular and consistent. The[]
are always at the same place, everywhere. – unknown reddit user
Advantages
- consistency: every time you see
[]
(in rtype) a generic type is described
Disadvatages
- every time you see
[]
in JavaScript an object property or array element is accessed. - conflicts with current rtype array syntax
-
might be resolved by writting
[Number]
instead ofNumber[]
We could write
Array[T]
instead ofT[]
in rtype. We might use[T]
as short version ofArray[T]
. One benefit of writing the generic type in brackets is better readablity of type expressions:(Number | String)[]
vs.[Number | String]
orArray[Number | String]
– maiermic
-
Parentheses Syntax
Similar to a regular JavaScript function.
Advantages:
- it looks similar to a function declaration
- it behaves similar to a function declaration: defines a type constructor (= function) that takes types as parameters and returns a type
– maiermic
Disadvantages:
- it looks similar to a function declaration
- it behaves similar to a function declaration: defines a type constructor (= function) that takes types as parameters and returns a type
Sorry for trolling like messages. That’s my personal opinion. We should lean towards readability. Using parenthesis for everything damages readability significantly. (One of the reasons to not like Lisp is the parenthesis bloat.) – koresar
Advantages
-
type constructor are functions
Generic types like
Map
are not just type names. They are type constructors:type constructor is a feature of a typed formal language that builds new types from old ones Wikipedia - Type constructor
That means a type constructor is not a type itself, but it describes a class of types. – maiermic
A type constructor is more like a function that returns a type:
// type constructor Map function Map(t: Type, u: Type): Type { return ((x: t) => u, list: t[]) => u[] }
– maiermic
Disadvatages
- a type constructor might get confused with a regular function
- if types are inferred, type arguments are passed implicitly, which is not possible in regular JavaScript functions
Whitespace Syntax
Similar to Elm and Haskell.
Advantages
- fewer characters (no special brackets) are needed
- reusable/composable: due to curring, partial function application of type constructors is possible
Disadvatages
-
whitespace syntax introduces currying. Most JavaScript developers are not familiar with this concept of function notation.
Haskell is known but an exotic language yet. For C++/Java/C# developers it is counter intuitive that the space symbol have a special syntactic meaning. Frankly speaking, I still do not understand what’s that:
Transform t u: (x: t) => u
. – koresar
Issue Analytics
- State:
- Created 7 years ago
- Reactions:5
- Comments:37 (17 by maintainers)
Top GitHub Comments
@koresar
If we introduce generic rest parameters (see #101), it would be ambiguous. For example: Is
Collection[]
an array of collections or is it an empty collection (zero type arguments passed to genericinterface Collection [...T] { ... }
)?Furthermore, arrays of generic elements, e.g.
Promise[String][]
, look quite strange/confusing (to me). Even regarding another generic syntax like angle bracketsPromise<String>[]
I would prefer[...Promise<String>]
orArray<Promise<String>>
because I find it easier to parse/grasp that it is an array of some promises and not a promise of something. That’s because in case ofPromise<String>[]
you have to parse the hole element type (in this casePromise<String>
) first until you see[]
and know that it is actually an array of that type.I admit that
Promise[[...String]]
orPromise[[String]]
might look confusing, too, due to the double square brackets. However, it is less confusing, if you are more explicit:Promise[Array[...String]]
orPromise[Array[String]]
.Even though
String[]
might be more familiar to people familar with other languages like Java, I find[Number, Number, ...Number]
obvious (in JS world), since you can create an array in a similar notation in JavaScript[1, 2, ...rest]
.Perfect justification @maiermic I’m with you on that. Thank you