Skip to content

Working with Types

William Reynolds edited this page Jan 11, 2020 · 2 revisions

Please note that there are far better resources on the web for learning about type systems and what advantages they provide. This article describes how types are used within this wiki.

This article covers:

What a Type Is

A type describes data. It can describe a little bit about data, or a lot. Cardinality, valid values, operations, and even intended purpose can all be communicated with a good type. boolean is a very simple example.

boolean has a cardinality of 2

It has two valid values: true or false

With respect to intended purpose, boolean is meant to be used in boolean algebra – we know that intuitively because of its name. There is absolutely no reason you can't write a binary number as a boolean[] though! The boolean type communicates its purpose through its name, and its value names.

On Cardinality

Cardinality is the magnitude of possible values of a type. It is important with respect to the complexity of a function, class, interface, type alias, or an application as a whole. Generally speaking, low cardinality results in better understanding. Use the right type for the job, but consider a type that limits cardinality where possible.

Further Examples

number

number has a cardinality of 18014398509481982 (Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER)

Every value from Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER is a valid number value

number has a fairly obvious purpose... to be a number. Specifically, real numbers in the given range. Imaginary and complex numbers are not included.

IPerson

export interface IPerson {
  readonly name: string
  readonly age: number
  sayHi(): string
}

IPerson has an very, very large cardinality; basically infinity. The cardinality of string multiplied by the cardinality of number multiplied by the cardinality of string again for the return value of sayHi().

Any object having properties name, age, and sayHi, where name is a string, age is a number, and sayHi is a function which takes no arguments and returns a string, is a valid IPerson.

IPerson clearly describes its intent as a description of a person, and how that person might greet another person.

Type Notation

This wiki uses a type notation that, I think, was popularized by Haskell. Haskell behaves very differently than Javascript, so the type notation used here is an adaptation. The particular adaptation used is defined in the fantasy-land readme. This section will introduce examples of how the notation is translated to Typescript, but it will not explain concepts like type variables, generics, or currying.

Some novel concepts exist within this notation, like "Type Constructors". If one of these concepts demand further explanation, just open an issue.

Small note: All types and type variables are capitalized in this notation.

Examples

toUpper

Typescript:

declare function toString(s: string): string

Type notation:

toString :: String -> String

Note that type notation elides argument names.

Record.values

declare class Record<K, T> {
 static values<K, T>(r: Record<K, T>): Array<T>
}

Type notation:

Record.values :: Record k t -> Array t

Information about class implementations aren't necessary. Generics aren't wrapped with extraneous symbols.

map

Typescript:

declare function map<A, B>(f: (a: A) => B): (fa: Functor1<A>) => Functor1<B>

Type notation:

map :: Functor f => (a -> b) -> f a -> f b

Generics don't need to be declared initially in type notation. The Functor constraint can be expressed briefly, as "f is any instance of Functor". Currying is implied through multiple "returns". Also note that Functor1 (a real interface) needs to be declared in Typescript, whereas type notation allows us to specify a functor of arbitrary arity. Functor f could represent Maybe a or Either b a but Functor1 can only refer to Maybe a while Functor2 would be necessary to refer to Either b a.

map as a prototype method

Typescript:

declare interface Maybe<A> {
  map<B>(f: (a: A) => B): Maybe<B>
}

Type notation:

map :: Maybe a ~> (a -> b) -> Maybe b

The ~> indicates a prototype function in the context of typescript.

When to Write a New Type

Or more likely, when to write new instances of categories like Functor or Alternative for some type you have. This is a tricky question to answer and depends so much on your use case that it's hard to know. In general, avoid it. Writing instances of categories interfaces for your own types is time consuming, and you may actually get the same benefit from wrapping your data with generic instances like Maybe. That being said, playing around is a great way to learn, so go ahead and try it out.

Remember that these common instances of categories are used to encapsulate some kind of common problem within the type itself. Maybe encapsulates the handling of undefined or null, but you can try to make an enhanced Maybe that also becomes Nothing when given an empty string or object. See how that works out. Either encapsulates the provision of a default value. IO encapsulates some input or output operation, also allowing it to be deferred until a convenient time.

The generic instances are all about encapsulating specific logic, and the categories themselves allow all of the instances to have a common interface with intuitive functionality.