A lightweight approach to typing in plain JavaScript.
You might enjoy some advantages of more common type systems without the need to change your language and tooling. You might enjoy some unexpected advantages as well.
Let’s look at the practical aspects first. – If you are really interested, more general and slightly deeper thoughts on typing are further down.
> fooT = createType([ 'bar' ])
…
> fooT.get.baz
Thrown:
TypeError: Unknown property 'baz'
…
By default, all operations are immutable. However, immutability is not enforced in any fashion on the objects.
> fooT = createType(['bar'])
…
> foo1 = fooT.set.bar(0)({})
{ bar: 0 }
> foo2 = fooT.set.bar(1)(foo1)
{ bar: 1 }
> foo1
{ bar: 0 }
> fooT = createType([ 'bar' ])
…
> fooT.set.bar(0, {})
{ bar: 0 }
> fooT.set.bar(0)({})
{ bar: 0 }
>
All functions need no binding and thus lend themselves perfectly for functional composition.
> fooT = createType([ 'bar' ])
> r.pipe(
() => [ null, {}, { bar: 0 }, fooT.objOf.bar(1) ],
r.filter(fooT.has.bar),
fooT.pluck.bar
)()
[ 0, 1 ]
objOf
, has
and pluck
in the above example are inspired by Ramda lingo.
So are pick
, pickAll
, eq
, over
.
In practice I have found it very helpful to define aliases for props.
- Under some circumstances the cost for a deep rename might be too high.
- Or you might be unable to change the prop name at all, e.g. when using an external API.
- Or you might actually want to have both, a simple name and a very precise one. Say you have a prop called
f72OriginalClosingDay
. This name is very precise in the domain of Schlussnoten and we might want to keep it for reference. At the same time, it is a pain to use. So we might want to alias it tooriginalClosing
.
> fooT = createType([ {someVeryLongAndOrCumbersomeName: { alias: 'bar' }} ])
…
> foo = fooT.set.someVeryLongAndOrCumbersomeName(0)({})
{ someVeryLongAndOrCumbersomeName: 0 }
> fooT.get.bar(foo)
0
You get getters and setters out of the box. If you want, you can enhance them.
> fooT = createType([ {bar: { get: r.defaultTo(0) }} ])
…
> fooT.get.bar({})
0
> fooT = createType([ {bar: { set: Math.floor }} ])
…
> fooT.set.bar(0.5)({})
{ bar: 0 }
Interfaces, inheritance, mixins are largely unexplored at the moment. The value of this approach to typing seems to lie elsewhere, either way.
> fooT = createType([ 'bar' ])
{
props: { bar: [Function] },
get: { bar: [Function: f1] },
pick: { bar: [Function] },
pluck: { bar: [Function: f1] },
has: { bar: [Function] },
eq: { bar: [Function] },
set: { bar: [Function: f2] },
objOf: { bar: [Function] },
over: { bar: [Function: f2] }
}
Please refer to the living doc for details and examples. Should anything be unclear, kindly open an issue, or even a PR.
Types, huh, yeah!
What are they good for?– Edwin Starr
I just thought of this and it made me laugh. It is not my actual position on types. Nor is it Edwin Starr’s.
So what are types good for? A few things come to mind immediately.
- Autocompletion (simple tooling)
- Refactoring (advanced tooling) – Nothing in the JS world (nor even in the TS world at the moment) comes even close to what an IDE can do to Java safely.
- Documentation
- Type safety
When we discuss types in the broader JS ecosystem, in my perception, we talk almost exclusively only about these aspects. These are all valuable things to have. They are not free, however. I generally agree with Eric Elliott regarding the Type(Script) Tax. Loosely summarized: 1) JS tooling isn’t too bad1; 2) if you practice TDD and code reviews, type safety does not save you from too many additional bugs; 3) the cost of working with TS types is greater than the incremental advantages of TS over JS.
But wait, there is more. Nevermind structural typing. We need a place where to put the behavior of our things. As our things grow and can do more, we naturally put related behaviors close to each other and unrelated behaviors farther away. As we build cohesive modules, we naturally build types.
This has always been my main motivation for types. More importantly, I naturally gravitated towards building types in this sense in JS, a language that does not exactly force such constructs on you. When building cohesive behavior, of course, you also access the same properties time and time again.
This coincided with the desire for a simple way to encapsulate property access. It was driven by the wish for some safety. This became a major concern for me when a team that I was working with was writing quite a few tests just in order to guard itself against mistyping property names. The team gave Flow a try, but found the type tax too high. – I also wanted to be able to progressively enhance the property access.
Finally, because of an intensive use of Ramda, functional composition became a major concern.
The marriage of these three – behavior honeypot, property access and FP – led me to this type construct.
I use createType
to scaffold the property access. I then add interesting behavior as it emerges.
- On episode #90 of JS Party, Chris Hiller shared how he used TS tooling with JSDoc in order to type-check plain JS. I have not tried this. But it sounds as if you could get most advantages of TS’s type checking without switching to TS. See also https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html.
“Keep it simple stupid” came to mind immediately when I started looking for a name. There is a distinct lack of sophistication to this approach. Yet, it is useful. – So, “KISS” and “type” were there. I liked the association of “kiss and tell”. So I kept it.
Added bonus: 😽 and type.
- Abelson & Sussman for “Structure and Interpretation of Computer Programs”. Polaris.
- Leonie Dreschler-Fischer at the University of Hamburg for introducing me to FP.
- Ramda for making FP practical in JS.
- My colleagues in recent years, who supported me in exploring FP and in developing this approach to typing.
yarn run check
lints and runs testsyarn run release
checks and publishes
Typical concerns
- semantics: use enhancer or not?
- hypothesis: in most cases probably yes