-
Notifications
You must be signed in to change notification settings - Fork 0
Working with Types
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:
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.
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.
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.
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.
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.
Typescript:
declare function toString(s: string): string
Type notation:
toString :: String -> String
Note that type notation elides argument names.
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.
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
.
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.
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.