Skip to content

Instantly share code, notes, and snippets.

@timjb
Last active March 3, 2020 20:19
Show Gist options
  • Save timjb/8b0f1b734c3cbb1645d0dd8f63c164fb to your computer and use it in GitHub Desktop.
Save timjb/8b0f1b734c3cbb1645d0dd8f63c164fb to your computer and use it in GitHub Desktop.
Vortrag über Programmierung auf Typebene in TypeScript
namespace BigNats {
type RepeatHelper<N extends number, T extends any, CurrList extends T[], CurrSizes extends number[]> =
CurrSizes extends [1]
? (CurrList["length"] extends N ? ["found", CurrList] : ["not-found", Cons<T,CurrList>])
: {
"true": RepeatHelper<N, T, CurrList, Tail<CurrSizes>> extends ["not-found", infer NewCurrList]
? RepeatHelper<N, T, Cast<NewCurrList, T[]>, Tail<CurrSizes>>
: RepeatHelper<N, T, CurrList, Tail<CurrSizes>>
}[CurrSizes extends [-1] ? "not-going-to-happen" : "true"];
type InitialSizes = [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1];
type Repeat<N extends number, T extends any> = RepeatHelper<N, T, [], InitialSizes>[1];
type EightHundredAndSixtySeven = Repeat<867, any>;
}
namespace Integers {
type Length<L extends any[]> = L['length'];
type ListNat = undefined[];
type ListSucc<LN extends ListNat> = Cons<undefined, LN>;
type ListPred<LN extends ListNat> = Tail<LN>;
type ToListNat<N extends number> = ToListNatRec<N, []>;
// type ToListNatRec<N extends number, LN extends any[]> =
// Length<LN> extends N ? LN : ToListNatRec<N, ListSucc<LN>>;
type ToListNatRec<N extends number, LN extends ListNat> =
{
"true": LN,
"false": ToListNatRec<N, ListSucc<LN>>,
}[Length<LN> extends N ? "true" : "false"];
// type ListNatAdd<LN extends ListNat, LM extends ListNat> =
// ((...args: LN) => void) extends (head: undefined, ...tail: infer LNPrime) => void
// ? ListSucc<ListNatAdd<LNPrime, LM>>
// : LM;
// type ListNatAdd<LN extends ListNat, LM extends ListNat> =
// {
// "true": LM,
// "false": ListSucc<ListNatAdd<ListPred<LN>, LM>>,
// }[Length<LN> extends 0 ? "true" : "false"];
type TypeLevelError<DefinitionName extends string, ArgumentName extends string, T extends any, ExpectedSupertype extends any> =
T extends ["Error in application of", string, ": argument", string, "does not extend", any, ". Actual type is", any]
? T
: ["Error in application of", DefinitionName, ": argument", ArgumentName, "does not extend", ExpectedSupertype, ". Actual type is", T];
type AddNatToListNat<N extends number, LM extends any> =
LM extends ListNat
? AddNatToListNatRec<N, LM, []>
: TypeLevelError<"AddNatToListNat", "LM", LM, ListNat>;
type AddNatToListNatRec<N extends number, LM extends ListNat, LN extends ListNat> =
{
"true": LM,
"false": AddNatToListNatRec<N, ListSucc<LM>, ListSucc<LN>>,
}[Length<LN> extends N ? "true" : "false"];
type ToNumber<LN extends any> =
LN extends ListNat ? Length<LN> :
LN extends Minus<ListNat> ? Minus<Length<Value<LN>>> :
TypeLevelError<"ToNumber", "LN", LN, ListNat | Minus<ListNat>>;
type AddNat<N extends number, M extends number> = ToNumber<AddNatToListNat<N, ToListNat<M>>>;
// type Minus<N> = ["-", N];
// type Value<T extends Minus<any>> = T[1];
interface Minus<N> {
value: N;
}
type Value<T extends Minus<any>> = T["value"];
type Integer = number | Minus<number>;
type ListNatMinusListNat<LN extends any, LM extends any> =
LN extends ListNat
? (LM extends ListNat
? ListNatMinusListNatHelper<LN, LM>
: TypeLevelError<"ListNatMinusListNat", "LM", LM, ListNat>)
: TypeLevelError<"ListNatMinusListNat", "LN", LN, ListNat>;
type ListNatMinusListNatHelper<LN extends ListNat, LM extends ListNat> =
{
"LM-is-zero": LN,
"LN-is-zero": Minus<LM>,
"both-are-succ": ListNatMinusListNat<ListPred<LN>, ListPred<LM>>,
}[Length<LM> extends 0 ? "LM-is-zero" : Length<LN> extends 0 ? "LN-is-zero" : "both-are-succ"];
type SubtractNat<N extends number, M extends number> = ToNumber<ListNatMinusListNat<ToListNat<N>, ToListNat<M>>>
type Add<N extends Integer, M extends Integer> =
N extends number ?
(M extends number ? AddNat<N, M> :
M extends Minus<number> ? SubtractNat<N, Value<M>> :
TypeLevelError<"Add", "M", M, Integer>) :
N extends Minus<number> ?
(M extends number ? SubtractNat<M, Value<N>> :
M extends Minus<number> ? Minus<AddNat<Value<N>, Value<M>>> :
TypeLevelError<"Add", "M", M, Integer>) :
TypeLevelError<"Add", "N", N, Integer>;
// Examples:
type ThreePlusMinusSeven = Add<3, Minus<7>>;
type TwoPlusThree = Add<2, 3>;
type MinusFourPlus3 = Add<Minus<4>, 3>
type MinusSevenPlusMinusEight = Add<Minus<7>, Minus<8>>;
interface Quantity {
length: Integer;
time: Integer;
mass: Integer;
}
type Velocity = {
length: 1;
time: Minus<1>;
mass: 0;
};
type Newton = {
length: 1;
time: Minus<2>;
mass: 1;
}
type Frequency = {
length: 0;
time: Minus<1>;
mass: 0;
}
// type MultQuantities<Q extends Quantity, R extends Quantity> = {
// length: Add<Q["length"], R["length"]>;
// time: Add<Q["time"], R["time"]>;
// mass: Add<Q["mass"], R["mass"]>;
// }
type MultQuantities<Q extends Quantity, R extends Quantity> = {
[Dimension in keyof Quantity]: Add<Q[Dimension], R[Dimension]>;
}
type Watt = MultQuantities<Newton,Frequency>;
}
-- | De Bruijn Index
type DBI = Int
data LC
= Var DBI
| App LC LC
| Abs LC
| Succ
| Nat Int
deriving (Show)
-- TODO: hier ein paar Beispiele
sub :: LC -> DBI -> LC -> LC
sub term v substitute =
case term of
Var dbi -> if dbi == v then substitute else term
App fun arg -> App (sub fun v substitute) (sub arg v substitute)
Abs body -> Abs (sub body (succ v) substitute) -- nur korrekt, falls substitute keine freien Variablen enthält
_ -> term
eval :: LC -> LC
eval term =
case term of
App fun arg ->
case eval fun of
Abs body -> eval (sub body 0 arg)
Succ ->
case eval arg of
Nat i -> Nat (succ i)
_ -> error "keine Zahl!"
_ -> error ":-("
_ -> term
-- Church Encoding
-- https://en.wikipedia.org/wiki/Church_encoding
infixl 6 €
(€) :: LC -> LC -> LC
(€) = App
zero :: LC
zero = Abs (Abs (Var 0)) -- \f. \x. x
one :: LC
one = Abs (Abs (Var 1 € Var 0)) -- \f. \x. f x
plus :: LC -- \m. \n. \f. \x. m f (n f x)
plus = Abs (Abs (Abs (Abs (Var 3 € Var 1 € (Var 2 € Var 1 € Var 0)))))
two :: LC
two = plus € one € one
-- TODO: mult?
mult :: LC -- \m. \n. \f. \x. m (n f) x =
-- \m. \n. \f. m (n f)
mult = Abs (Abs (Abs (Var 2 € (Var 1 € Var 0))))
potenz :: LC -- \m. \n. \f. \x. n m f x =
-- \m. \n. n m
potenz = Abs (Abs (Var 0 € Var 1))
toNat :: LC
toNat = Abs (Var 0 € Succ € Nat 0)
nonTerminating :: LC
nonTerminating = applyToSelf € applyToSelf
where applyToSelf = Abs (Var 0 € Var 0)
-- TODO: Y Combinator?
namespace LambdaCalculus {
// Variables
interface Var<DBI extends any[]> {
var: DBI; // De Bruijn index
}
// Application
interface App<Fun extends LC, Arg extends LC> {
fun: Fun;
arg: Arg;
}
// Abstraction (Lambdas!)
interface Abs<Body extends LC> {
body: Body;
}
interface SuccFun {
succFun: "succFun";
}
interface Nat<Val extends any[]> {
val: Val
}
type LC = Var<any[]> | App<LC, LC> | Abs<LC> | SuccFun | Nat<any[]>
type Sub<Term extends LC, V extends any[], Substitute extends LC> =
Term extends Var<infer DBI> ?
(DBI extends V ? Substitute : Term) :
Term extends App<infer Function, infer Arg> ?
App<Sub<Function,V,Substitute>,Sub<Arg,V,Substitute>> :
Term extends Abs<infer Body> ?
Abs<Sub<Body,Succ<V>,Substitute>> : // nur korrekt, falls Substitute keine freien Variablen enthält
Term;
// Erster Versuch:
// type Eval<Term extends LC> =
// Term extends App<infer Function, infer Arg>
// ? (Eval<Function> extends Abs<infer Body> ? Sub<Body, [], Arg> : ":-(")
// : Term;
// Zweiter Versuch:
type Eval<Term extends LC> =
Term extends App<infer Fun, infer Arg>
? ({
"Rekursion":
Eval<Fun> extends infer E ?
E extends Abs<infer Body> ?
Eval<Sub<Body, [], Arg>> :
E extends SuccFun ?
(Eval<Arg> extends Nat<infer Val> ? Nat<Succ<Val>> : "keine Zahl!") :
":-(" :
"passiert nicht";
}[Fun extends Var<[]> ? "Rekursion" : "Rekursion"])
: Term;
type V<DBI extends number> = ToAnyList<DBI> extends infer N ? Var<Cast<N, any[]>> : "too bad";
type Zero = Abs<Abs<V<0>>>
type One = Abs<Abs<App<V<1>,V<0>>>>
type PlusLC =
Abs<Abs<Abs<Abs<
App<
App<V<3>, V<1>>,
App<
App<V<2>,V<1>>,
V<0>
>
>
>>>>;
type Two = App<App<PlusLC,One>,One>;
type ToNat = Abs<App<App<V<0>,SuccFun>,Nat<[]>>>;
type Three = Eval<App<ToNat,App<App<PlusLC,One>,Two>>>;
type ApplyToSelf = Abs<App<V<0>,V<0>>>;
type NonTerminating = App<ApplyToSelf,ApplyToSelf>;
// type NonTerminating1 = Eval<NonTerminating>;
}
// Programmieren auf Typebene in TypeScript
// ========================================
// (getestet mit TypeScript 3.7.3)
// (online als Gist: https://gist.github.com/timjb/8b0f1b734c3cbb1645d0dd8f63c164fb)
// Diese Datei kann ausgeführt werden mit `ts-node livecoding.ts`
// Primitive Typen
const falsch: boolean = false;
const antwort: number = 42.23;
const beispielString: string = "foo";
// Zusammengesetzte Typen
const gruesse = (name: string) => `Hallo ${name.toUpperCase()}!`;
type Greeter = (name: string) => string;
// const gruesse: Greeter = name => `Hallo ${name.toUpperCase()}!`;
const gruss = gruesse("Curry Club");
// jetzt Typfehler zeigen bei zusaetzlichem Argument
const einObjekt = { einSchluessel: "foo" }
// type ObjektMitSchluessel = { einSchluessel: string }
// interface ObjektMitSchluessel {
// einSchluessel: string;
// }
// const einObjekt: ObjektMitSchluessel = { einSchluessel: "foo" }
// jetzt zeigen, dass TypeScript zusaetzliche Keys ablehnt
// Vereinigungstypen (union types)
type NummerOderString = number | string;
const eineNummer: NummerOderString = 313;
const einString: NummerOderString = "donald";
const undefiniert = undefined;
const billionDollarMistake = null;
const parseDecimalDigit: (char: string) => number | undefined = char => {
if (char.length !== 1) { return undefined; }
const charCode = char.charCodeAt(0);
if (48 <= charCode && charCode <= 57) { return charCode - 48; }
return undefined;
};
console.log(`parseHex('B') = ${parseDecimalDigit('B')}`);
console.log(`parseHex('x') = ${parseDecimalDigit('x')}`);
const irgendwas: any = 42;
// const irgendwas: any = "haskell";
// const irgendwas: any = undefined;
// const irgendwas: any = {};
const unbekannt: unknown = 42;
// console.log(gruesse(irgendwas)); // Laufzeitfehler!
// console.log(gruesse(unbekannt));
// die folgenden Typen erst ohne Feld "art" programmieren
interface Kreis {
art: "kreis"; // Literaltyp
radius: number;
}
// const einheitsKreis: Kreis = { radius: 1 };
const einheitsKreis: Kreis = { art: "kreis", radius: 1 };
interface Rechteck {
art: "rechteck";
breite: number;
hoehe: number;
}
// const quadrat: Rechteck = { breite: 1, hoehe: 1 }
const quadrat: Rechteck = { art: "rechteck", breite: 1, hoehe: 1 }
type Figur = Rechteck | Kreis;
const flaecheninhalt = (figur: Figur): number => {
switch (figur.art) {
case "rechteck":
return figur.breite * figur.hoehe;
case "kreis":
return figur.radius * figur.radius * Math.PI;
}
};
console.log(`flaecheninhalt(einheitsKreis) = ${flaecheninhalt(einheitsKreis)}`);
console.log(`flaecheninhalt(quadrat) = ${flaecheninhalt(quadrat)}`);
// Literaltypen gibt es auch für Zahlen
const vier: 4 = 4;
// und Booleans
const wahr: true = true;
// Array-Typen
const fibU50: number[] = [1,1,2,3,5,8,13,21,34];
// Generics
const init = <A>(arr: A[]): A[] => arr.slice(0, arr.length - 1);
console.log(`init(fibU50) = ${JSON.stringify(init(fibU50))}`);
// Tupel
type TupelTyp = [string, number, boolean];
const tupel: TupelTyp = ["prim", 10, false];
const tupelLänge = tupel.length;
// Indices
type KreisRadiusTyp = Kreis["radius"]
type FigurArten = Figur["art"];
type TupelLaenge = TupelTyp["length"];
// Rest-Argumente
const lispToMathNotation = (fnName: string, ...args: (string | number)[]) => `${fnName}(${args.join(', ')})`;
console.log(lispToMathNotation("pow", 2, 4));
//type RepeatStringFn = (...args: [string, number]) => string;
const doesExtend: "foo" extends string ? true : false = true;
// ausprobieren mit:
// "foo" extends number
// string extends number | string
// { foo: number } extends {}
// (() => string) extends (() => number | string)
// ((foo: number) => void) extends ((foo: number | string) => void)
// ((foo: number | string) => void) extends ((foo: number) => void)
// (() => void) extends ((foo: number) => void)
// Diagramm aller TypeScript-Typen zeigen
// https://gist.github.com/laughinghan/31e02b3f3b79a4b1d58138beff1a2a89
// infer-enz
type Rueckgabetyp<T> = T extends () => infer R ? R : "keine Funktion!";
type R1 = Rueckgabetyp<() => string>;
type R2 = Rueckgabetyp<boolean>;
type R3 = Rueckgabetyp<(foo: string) => boolean>;
// richtigerer Typ:
//type Rueckgabetyp<T> = T extends (...args: any[]) => infer R ? R : "keine Funktion!";
type Index<O,K extends string> = O extends { [k in K]: infer V } ? V : undefined;
type I1 = Index<{foo: "bar"}, "foo">;
type I2 = Index<{foo: 1, bar: 2}, string>;
type I3 = Index<3, string>;
// Tupeltypen als Ersatz für Listen auf Typebene
// ---------------------------------------------
type Head<T extends any[]> = T[0];
type Head1 = Head<["foo", "bar"]>;
type Cons<H, T extends any[]> =
((head: H, ...tail: T) => void) extends (...args: infer U) => void
? U
: T;
type Cons1 = Cons<boolean, [number, string]>;
type Cons2 = Cons<boolean, string[]>;
type Tail<T extends any[]> =
((...ts: T) => void) extends (_head: any, ...tail: infer T) => void
? T
: [];
type Tail1 = Tail<Cons1>;
// Jetzt implementieren wir eine rekursive Funktion
// reverseAppend :: [a] -> [a] -> [a]
// mit der Eigenschaft
// reverseAppend xs ys == reverse xs ++ ys
//
// Basisfall:
// reverseAppend [] ys = ys
//
// Rekursiver Schritt:
// reverseAppend (x:xs) ys = reverseAppend xs (x:ys)
// Erster Versuch:
// type ReverseAppend<Xs extends any[], Ys extends any[]> =
// Xs extends []
// ? Ys
// : ReverseAppend<Tail<Xs>, Cons<Head<Xs>, Ys>>;
// Richtig
type ReverseAppend<Xs extends any[], Ys extends any[]> =
{
"Basisfall": Ys,
"Rekursiver Schritt": ReverseAppend<Tail<Xs>, Cons<Head<Xs>, Ys>>,
}[Xs extends [] ? "Basisfall" : "Rekursiver Schritt"];
type ReverseAppend1 = ReverseAppend<[1, 2, 3, 4], [5, 6, 7, 8]>;
type Reverse<Xs extends any[]> = ReverseAppend<Xs, []>;
// nächste Definition erstmal überspringen:
type Cast<X, Y> = X extends Y ? X : Y;
type Cast1 = Cast<3, number>;
type Cast2 = Cast<string, number>;
// Implementiere (++):
// Erster Versuch:
// type Append<Xs extends any[], Ys extends any[]> = ReverseAppend<Reverse<Xs>, Ys>;
// Zweiter Versuch:
// type Append<Xs extends any[], Ys extends any[]> =
// Reverse<Xs> extends infer RevXs ? ReverseAppend<RevXs, Ys> : [];
// Wir brauchen Casting!
// Richtig:
type Append<Xs extends any[], Ys extends any[]> =
Reverse<Xs> extends infer RevXs ? ReverseAppend<Cast<RevXs, any[]>, Ys> : [];
type Append1 = Append<[1,2,3], [3,4]>;
// Arithmetik auf Typ-Ebene
// ------------------------
// Idee: Repräsentiere natürliche Zahlen durch Tupeltypen, die any[] extenden:
// 0 = []
// 1 = [any]
// 2 = [any,any]
// usw.
// Addition ist dann Zusammenhängen von Tupeln.
type Succ<Xs extends any[]> = Cons<any, Xs>;
type Succ1 = Succ<[any, any]>;
// Konvertierung einer so repräsentierten Zahl zu `number`:
type ToNumber<Xs extends any[]> = Xs["length"];
type ToNumber1 = ToNumber<[any, any, any]>;
// In die andere Richtung mittels "unbeschränkter" rekursiver Suche:
type ToAnyList<N extends number> = Cast<ToAnyListHelper<N, []>, any[]>;
type ToAnyListHelper<N extends number, Current extends any[]> =
{
"Found": Current,
"Keep searching": ToAnyListHelper<N, Succ<Current>>,
}[ToNumber<Current> extends N ? "Found" : "Keep searching"];
type ToAnyList1 = ToAnyList<7>;
type ToAnyList2 = ToAnyList<41>;
// type ToAnyList3 = ToAnyList<42>;
// type ToAnyList4 = ToAnyList<44>;
// type ToAnyList5 = ToAnyList<45>;
// Grund: Rekursionstiefenbeschränkung von 50 demonstrieren
// Addition auf `number`:
// type Plus<M extends number, N extends number> = ToNumber<ReverseAppend<ToAnyList<M>, ToAnyList<N>>>;
type Plus<M extends number, N extends number> =
ToAnyList<M> extends infer MList
? ToAnyList<N> extends infer NList
? ReverseAppend<Cast<MList, any[]>, Cast<NList, any[]>> extends infer A
? ToNumber<Cast<A, any[]>>
: "passiert ebenfalls nicht"
: "passiert auch nicht"
: "passiert nicht";
// Oder etwas kürzer:
// type Plus<M extends number, N extends number> =
// [ToAnyList<M>, ToAnyList<N>] extends [infer MList, infer NList]
// ? ReverseAppend<Cast<MList, any[]>, Cast<NList, any[]>> extends infer A
// ? ToNumber<Cast<A, any[]>>
// : "passiert ebenfalls nicht"
// : "passiert nicht";
type Plus1 = Plus<7,8>;
// BigNats.ts: Umgeht Größenbeschränkung durch binäre Suche
// Integers.ts: Negative Zahlen, Subtraktion und physikalische Größen
// TODO: Multiplikation?
// TODO: Potenzieren?
// Im rekursiven Fall muss ein "Aufruf" desselben Typs stehen.
// Das ist eine kleinere Einschränkung als es erst einmal erscheint.
// Currying
// --------
function simpleCurry<X, Xs extends any[], R>(fn: (firstArg: X, ...restArgs: Xs) => R): (firstArg: X) => (...restArgs: Xs) => R {
return firstArg => (...restArgs) => fn(firstArg, ...restArgs);
}
console.log(simpleCurry(lispToMathNotation)("K")("G", "n"));
type Curry<Args extends any[], R> = Args extends [] ? R : (x: Head<Args>) => Curry<Tail<Args>, R>;
function curry<Xs extends any[], R>(fn: (...args: Xs) => R): Curry<Xs, R> {
return curryHelper(fn, fn.length);
}
function curryHelper<Xs extends any[], R>(fn: (...args: Xs) => R, numArgs: Xs["length"]): Curry<Xs, R> {
if (hasNoArguments(fn, numArgs)) {
return (fn() as unknown) as Curry<Xs, R>;
}
return ((x: Head<Xs>) => curryHelper((...restArgs: Tail<Xs>) => (fn as (x: Head<Xs>, ...restArgs: Tail<Xs>) => R)(x, ...restArgs), numArgs - 1) as unknown) as Curry<Xs, R>;
};
function hasNoArguments<Xs extends any[], R>(fn: (...args: Xs) => R, numArgs: Xs["length"]): fn is (() => R) {
return numArgs === 0;
};
const det = (a: number, b: number, c: number, d: number) => a*d - b*c;
const det1 = curry(det)(4)(2)(2)(1);
console.log(`det1 = ${det1}`);
// Mit tonnenweise zusätzlicher Komplikationen:
// https://www.freecodecamp.org/news/typescript-curry-ramda-types-f747e99744ab/
// Lambda-Kalkül
// -------------
// siehe LC.hs
// Variables
interface Var<DBI extends any[]> {
var: DBI; // De Bruijn index
}
// Application
interface App<Fun extends LC, Arg extends LC> {
fun: Fun;
arg: Arg;
}
// Abstraction (Lambdas!)
interface Abs<Body extends LC> {
body: Body;
}
type LC = Var<any[]> | App<LC, LC> | Abs<LC>
type Sub<Term extends LC, V extends any[], Substitute extends LC> =
Term extends Var<infer DBI> ?
(DBI extends V ? Substitute : Term) :
Term extends App<infer Function, infer Arg> ?
App<Sub<Function,V,Substitute>,Sub<Arg,V,Substitute>> :
Term extends Abs<infer Body> ?
Abs<Sub<Body,Succ<V>,Substitute>> : // nur korrekt, falls Substitute keine freien Variablen enthält
Term;
// Erster Versuch:
// type Eval<Term extends LC> =
// Term extends App<infer Function, infer Arg>
// ? (Eval<Function> extends Abs<infer Body> ? Sub<Body, [], Arg> : ":-(")
// : Term;
// Zweiter Versuch:
type Eval<Term extends LC> =
Term extends App<infer Fun, infer Arg>
? ({
"Rekursion": Eval<Fun> extends Abs<infer Body> ? Eval<Sub<Body, [], Arg>> : ":-("
}[Fun extends Var<[]> ? "Rekursion" : "Rekursion"])
: Term;
type V<DBI extends number> = ToAnyList<DBI> extends infer N ? Var<Cast<N, any[]>> : "too bad";
type Zero = Abs<Abs<V<0>>>
type One = Abs<Abs<App<V<1>,V<0>>>>
type PlusLC =
Abs<Abs<Abs<Abs<
App<
App<V<3>, V<1>>,
App<
App<V<2>,V<1>>,
V<0>
>
>
>>>>;
type Two = Eval<App<App<PlusLC,One>,One>>;
// Fazit: Das Typsystem von TypeScript ist prinzipiell Turing-vollständig, wäre da nicht die Beschränkung von
// maximal 5.000.000 Instanziierungen / maximale Instanziierungstiefe 50 :-)
// Weitere Ideen
// -------------
// TODO: Distributivität von Union-Types?
// TODO: Versuch, Cons direkt zu implementieren?
{
"compilerOptions": {
"strict": true
},
"include": [
"./**/*.ts"
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment