Skip to content

Instantly share code, notes, and snippets.

@LiamGoodacre
Last active February 13, 2022 23:35
Show Gist options
  • Save LiamGoodacre/d3931312914d045a3f5c02aec36350a8 to your computer and use it in GitHub Desktop.
Save LiamGoodacre/d3931312914d045a3f5c02aec36350a8 to your computer and use it in GitHub Desktop.
TypeScript: flat domain modelling with applicability & invariants
const absurd = (v: never): never => v
const typed = <T,>(v: T): T => v
type Kinded<A extends B, B> = A
type Norm<T> = T extends T ? { [k in keyof T]: T[k] } : never
//
interface Condition { present: unknown; absent: unknown }
type Cond<P, A> = Condition & { present: P, absent: A }
type Not<C extends Condition> = Cond<C['absent'], C['present']>
type Always = Cond<unknown, never>
type Never = Not<Always>
type And<L extends Condition, R extends Condition> =
Cond<L['present'] & R['present'], L['absent'] | R['absent']>
type Or<L extends Condition, R extends Condition> =
Cond<L['present'] | R['present'], L['absent'] & R['absent']>
type Match<NS, Name extends keyof NS, T extends NS[Name] | null> =
Cond<Record<Name, T>, Record<Name, Exclude<NS[Name] | null, T>>>
interface SomeRule {
// under what condition is a field applicable
applicable: Condition
// when applicable, what condition should hold true
invariant: Condition
}
type Rule<NS, K extends keyof NS, R extends SomeRule> = Record<K, R>
type Field<NS, Name extends keyof NS, R extends SomeRule> =
| (R['applicable']['present'] & R['invariant']['present'] & Record<Name, NS[Name]>)
| (R['applicable']['present'] & R['invariant']['absent'] & Record<Name, never>)
| (R['applicable']['absent'] & Record<Name, null>)
// Interprets Rules to into an intersection of Fields, for a subset of field names
type Build<NS, KS extends keyof NS, RS> =
RS extends [infer H, ...infer T] ?
(
& (keyof H extends KS ? (
Field<NS, keyof H, (H[keyof H] & SomeRule)>
) : unknown)
& Build<NS, KS, T>
) :
unknown
type Property = null
interface SomeDomain {
names: {}
rules: Array<Record<string, SomeRule>>
}
type Compile<D extends SomeDomain, KS extends keyof D['names']> =
Norm<Build<D['names'], KS, D['rules']>>
type Inc<D extends SomeDomain, P, K extends keyof D['names']> =
Compile<D, (keyof P | K) & keyof D['names']>
type Valid<D extends SomeDomain> = Compile<D, keyof D['names']>
type Raw<D extends SomeDomain> = Partial<D['names']>
//
type Names = {
hasCats: "Yes" | "No"
favCat: "odin" | "thor" | "loki"
wellformed: Property
}
type Rules = [
Rule<Names, "hasCats", { applicable: Always, invariant: Always }>,
Rule<Names, "favCat", { applicable: Match<Names, "hasCats", "Yes">, invariant: Always }>,
Rule<Names, "wellformed", { applicable: Always, invariant: Not<Match<Names, "favCat", "odin">> }>,
]
type Domain = Kinded<{ names: Names, rules: Rules }, SomeDomain>
const eg0 = typed<Valid<Domain>>({ hasCats: "Yes", favCat: "loki", wellformed: null })
const eg1 = typed<Valid<Domain>>({ hasCats: "No", favCat: null, wellformed: null })
// @ts-expect-error
const eg2 = typed<Valid<Domain>>({ hasCats: true, favCat: "odin", wellformed: null })
// @ts-expect-error
const eg3 = typed<Valid<Domain>>({ hasCats: false, favCat: "thor", wellformed: null })
type Inc0 = Inc<Domain, {}, "hasCats">
type Inc1 = Inc<Domain, Inc0, "favCat">
const valid = (p: Raw<Domain>): null | Valid<Domain> => {
const inc0 = ((): null | Inc<Domain, {}, 'hasCats'> => {
if (p.hasCats === undefined) return null
return { hasCats: p.hasCats }
})()
if (!inc0) return null
const inc1 = ((): null | Inc<Domain, typeof inc0, 'favCat'> => {
switch (inc0.hasCats) {
case "Yes":
if (p.favCat === undefined) return null
if (p.favCat === null) return null
return { favCat: p.favCat, hasCats: inc0.hasCats }
case "No":
return { favCat: null, hasCats: inc0.hasCats }
default: return absurd(inc0.hasCats)
}
})()
if (!inc1) return null
const inc2 = ((): null | Inc<Domain, typeof inc1, 'wellformed'> => {
switch (inc1.favCat) {
case null: return { hasCats: inc1.hasCats, favCat: inc1.favCat, wellformed: null }
case "odin": return null
case "thor": return { hasCats: inc1.hasCats, favCat: inc1.favCat, wellformed: null }
case "loki": return { hasCats: inc1.hasCats, favCat: inc1.favCat, wellformed: null }
default: return absurd(inc1)
}
})()
if (!inc2) return null
return inc2
}
const k0 = (dom: Valid<Domain>): string => {
switch (dom.hasCats) {
case "Yes":
switch (dom.favCat) {
case "odin": return absurd(dom.wellformed)
case "thor": return ""
case "loki": return ""
default: return absurd(dom)
}
case "No":
switch (dom.favCat) {
case null: return ""
default: return absurd(dom)
}
default: return absurd(dom)
}
}
const k1 = (dom: Valid<Domain>): string => {
switch (dom.favCat) {
case "odin": return absurd(dom.wellformed)
case "thor":
switch (dom.hasCats) {
case "Yes":
switch (dom.wellformed) {
case null: return ""
default: return absurd(dom)
}
default: return absurd(dom)
}
case "loki":
switch (dom.hasCats) {
case "Yes": return ""
default: return absurd(dom)
}
case null:
switch (dom.hasCats) {
case "No": return ""
default: return absurd(dom)
}
default: return absurd(dom)
}
}
/*
@enum([
{ value: "Yes" },
{ value: "No" }
])
string HasCats
@enum([
{ value: "odin" },
{ value: "thor" },
{ value: "loki" }
])
string FavCat
unit Property
structure Domain {
@applicable(Always)
@invariant(Always)
hasCats: HasCats
@applicable(Match(hasCats, "Yes"))
@invariant(Always)
favCat: FavCat
@applicable(Always)
@invariant(Not(Match(favCat, "odin")))
wellformed: Property
}
*/
namespace Code {
type Shape =
| { type: "Enum", options: string[] }
| { type: "Property" }
type Expression =
| { type: "Always" }
| { type: "Never" }
| { type: "And", lhs: Expression, rhs: Expression }
| { type: "Or", lhs: Expression, rhs: Expression }
| { type: "Not", exp: Expression }
| { type: "Match", name: string, value: string }
type Entry = {
name: string
shape: Shape
applicable: Expression
invariant: Expression
}
type Data = Array<Entry>
const eg: Data = [
{
name: "hasCats",
shape: { type: "Enum", options: ["Yes", "No"] },
applicable: { type: "Always" },
invariant: { type: "Always" }
},
{
name: "favCat",
shape: { type: "Enum", options: ["odin", "thor", "loki"] },
applicable: { type: "Match", name: "hasCats", value: "Yes" },
invariant: { type: "Always" }
},
{
name: "wellformed",
shape: { type: "Property" },
applicable: { type: "Always" },
invariant: { type: "Not", exp: { type: "Match", name: "favCat", value: "odin" } }
},
]
const expressionToCode = (exp: Expression): string => {
switch (exp.type) {
case "Always": return `Always`
case "Never": return `Never`
case "And": return `And<${expressionToCode(exp.lhs)}, ${expressionToCode(exp.rhs)}>`
case "Or": return `Or<${expressionToCode(exp.lhs)}, ${expressionToCode(exp.rhs)}>`
case "Not": return `Not<${expressionToCode(exp.exp)}>`
case "Match": return `Match<Names, ${JSON.stringify(exp.name)}, ${JSON.stringify(exp.value)}>`
default: return absurd(exp)
}
}
const shapeToCode = (shape: Shape): string => {
switch (shape.type) {
case "Enum": return (
shape.options.length === 0
? "never"
: shape.options.map(x => JSON.stringify(x)).join(" | ")
)
case "Property": return `Property`
default: return absurd(shape)
}
}
const dataToCode = (data: Data): string => {
const names: string[] = []
const rules: string[] = []
for (const field of data) {
names.push(`${field.name}: ${shapeToCode(field.shape)}`)
rules.push(`Rule<Names, ${JSON.stringify(field.name)
}, { applicable: ${expressionToCode(field.applicable)
}, invariant: ${expressionToCode(field.invariant)
} }>,`)
}
return `
type Names = {
${names.join("\n ")}
}
type Rules = [
${rules.join("\n ")}
]
type Domain = Kinded<{ names: Names, rules: Rules }, SomeDomain>`
}
console.log(dataToCode(eg))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment