question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

generic types - syntax comparison

See original GitHub issue

Overview 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

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>(), but instance.<String>method() at use-site; and at declaration-site (different issue) it’s even more different with class 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 user

    Do 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 user

    In 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?
    

    ericelliott

Square Brackets Syntax

Similar to Scala generics

Java’s approach is saying “f*ck it”, let’s just have stuff like method<String>(), but instance.<String>method() at use-site; and at declaration-site (different issue) it’s even more different with class 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 of Number[]

      We could write Array[T] instead of T[] in rtype. We might use [T] as short version of Array[T]. One benefit of writing the generic type in brackets is better readablity of type expressions: (Number | String)[] vs. [Number | String] or Array[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:closed
  • Created 7 years ago
  • Reactions:5
  • Comments:37 (17 by maintainers)

github_iconTop GitHub Comments

3reactions
maiermiccommented, May 14, 2016

@koresar

May I propose one more array notation? - []

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 generic interface Collection [...T] { ... })?

Furthermore, arrays of generic elements, e.g. Promise[String][], look quite strange/confusing (to me). Even regarding another generic syntax like angle brackets Promise<String>[] I would prefer [...Promise<String>] or Array<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 of Promise<String>[] you have to parse the hole element type (in this case Promise<String>) first until you see [] and know that it is actually an array of that type.

I admit that Promise[[...String]] or Promise[[String]] might look confusing, too, due to the double square brackets. However, it is less confusing, if you are more explicit: Promise[Array[...String]] or Promise[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].

1reaction
koresarcommented, May 14, 2016

Perfect justification @maiermic I’m with you on that. Thank you

Read more comments on GitHub >

github_iconTop Results From Across the Web

C#. Comparison of instances of generic types - BestProg
Comparison of instances of generic types. IComparable<T> and IEquatable<T> interfaces. In this topic, you will learn how to properly compare ...
Read more >
Generic Types - Learning the Java Language
A generic type is a generic class or interface that is parameterized over types. The following Box class will be modified to demonstrate...
Read more >
c# - How to compare values of generic types? - Stack Overflow
What keeps us from comparing the values of generic types which are known to be IComparable ? Doesn't it somehow defeat the entire...
Read more >
Generics in Java - GeeksforGeeks
Generic Classes : A generic class is implemented exactly like a non-generic class. The only difference is that it contains a type parameter ......
Read more >
Generic Types - Visual Basic | Microsoft Learn
A generic type is a single programming element that adapts to perform the same functionality for a variety of data types.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found