Skip to content

Instantly share code, notes, and snippets.

@ducin
Last active January 12, 2022 13:29
Show Gist options
  • Save ducin/a7a187249fae26977c8f98a31a61216d to your computer and use it in GitHub Desktop.
Save ducin/a7a187249fae26977c8f98a31a61216d to your computer and use it in GitHub Desktop.
Functional Composition with Static Types in TypeScript, Tomasz Ducin, Warsaw TypeScript #2, 2019.02.06
/**
* FUNCTIONAL COMPOSITION
*
* Tomasz Ducin
* http://ducin.it
* twitter: @tomasz_ducin
*/
// some initial stuff
type ContractType = "contract" | "permanent";
type Developer = {
"id": number;
"nationality": "PL" | "DE",
"salary": number;
"firstName": string;
"lastName": string;
"contractType": ContractType;
"skills": string[];
};
declare const devs: Developer[]
///////////////////////////////////////
let fn: Function
type FnType = () => number
type Closure = () => () => () => number
type NoopFn = () => void
var noopFn: NoopFn = () => {}
///////////////////////////////////////
interface InterfaceFunction {
(): void
get: Function
post: Function
}
// $("div")
// $.get(...)
// $.post(...)
// $.ajax(...)
// TYPE INFERENCE
// no inference for function params
function add(a: number, b: number){
return a + b
}
// TYPE COMPATIBILITY
type City = 'Warsaw' | 'Wrocław'
type CityFn = (arg: City) => void
type StringFn = (arg: string) => void
var cityFn: CityFn
var stringFn: StringFn
cityFn = stringFn
stringFn = cityFn // fails with strictFunctionTypes (contravariance)
///////////////////////////////////////
// GENERICS
type FnParam<T> = (t: T) => boolean
type FnGeneric = <T>(t: T) => boolean
type DeveloperBoolFn = FnParam<Developer>
var f5: FnGeneric = (d: Developer) => true // FAILS!
// assigned function needs to have a generic, fails because it's a certain type, not a generic
// only generic fn can be assigned to a generic-fn type
declare const f6: FnGeneric;
f6(devs[0])
f6(1)
// (1) input parameters and output type has to match
// (2) you can pass a fn that accepts less params, but not more params
const add1 = (a) => a + 1
var arr = [1, 2, 3].map((a, idx, arr) => a + 1)
///////////////////////////////////////
// example 1: Composing Boolean Functions - negate
type EntityBoolFn<T> = (t: T) => boolean
// type DevBoolFn = (d: Developer) => boolean
type DevBoolFn = EntityBoolFn<Developer>
const devHasContractType =
(c: ContractType) =>
(d: Developer) => d.contractType === c
const devIsContractor = devHasContractType("contract")
// const devIsPermanent = devHasContractType("permanent")
type BoolComposition = (dfn: DevBoolFn) => DevBoolFn
const negate: BoolComposition =
(bfn: DevBoolFn) =>
(d) => !bfn(d)
const devIsPermanent = negate(devIsContractor)
///////////////////////////////////////
// example 2: Composing Boolean Functions - all
const all = (dfns: DevBoolFn[]): DevBoolFn =>
(d) => dfns.every(dfn => dfn(d))
// 2nd : reduce
// 3rd : recursion
// 4th : some
// 5th : find
// /\/\/\
// |____|
// REDUCE
// (bigger is NOT what we need here)
let bigger = [1,2,3].every(a => a > 0)
///////////////////////////////////////
// example 3: Limitation of TS: mergeMultipleObjects
// reduce ran against dynamic number of arguments, each with potentially different shape
const merge2Objects = <T extends Object, U extends Object>
(obj1: T, obj2: U) =>
({ ...obj1, ...obj2 })
function mergeMultipleObjects<T extends Object, U extends Object>(t: T, u: U): T & U
function mergeMultipleObjects<T extends Object, U extends Object, W extends Object>(t: T, u: U, w: W): T & U & W
function mergeMultipleObjects<T extends Object, U extends Object, W extends Object, X extends Object>(t: T, u: U, w: W, x: X): T & U & W & X
function mergeMultipleObjects(objects: []) {
return objects.reduce(merge2Objects)
}
let r1 = mergeMultipleObjects(
{name: 'John'},
{age: 40},
{city: "Liverpool"},
{band: "The Beatles"})
console.log(devIsContractor(devs[1]) )
console.log(devIsPermanent(devs[1]) )
///////////////////////////////////////
// example 4: Limitation of TS: pipe
// the same limitation, reduce with different shapes (function signatures)
function pipe<T, U, V>(
fn1: (t: T) => U,
fn2: (u: U) => V
): (t: T) => V
function pipe<T, U, V, W>(
fn1: (t: T) => U,
fn2: (u: U) => V,
fn3: (v: V) => W,
): (t: T) => W
function pipe<T, U, V, W, X>(
fn1: (t: T) => U,
fn2: (u: U) => V,
fn3: (v: V) => W,
fn4: (w: W) => X,
): (t: T) => X
function pipe(...fns: Function[]){ // left -> right
return (value) => fns.reduce( (v, fn) => fn(v), value )
}
let processing = pipe(
(devs: Developer[]) => devs.filter(d => d.nationality === 'PL'),
(devs) => devs.filter(d => d.skills.includes('TypeScript')),
(devs) => devs.reduce((maxSalaryDev, d) =>
maxSalaryDev.salary > d.salary ? maxSalaryDev : d, devs[0]), // max salary
(dev) => `${dev.firstName} ${dev.lastName}`
)
const endOfThisLiveCodingSession = processing(devs)
// enjoy the magnificent types within the pipe!
{
"compilerOptions": {
"lib": [
"es2018"
],
"strictFunctionTypes": true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment