It has all the pieces!
- data:
- objects, arrays, and tuples
- functions
- structured programming:
- conditionals
- pattern matching
- recursion
- fun
- familiarity with type-level programming in TS
- knowing the limits
- seeing beyond the limits
- Analogies can be helpful learning aids:
- The programming language metahpor can help us:
- guess when TS can solve a poblem
- guess when TS probably can't
- What would type-level TS look like if it was even more like a normal programming language?
type Id = number;
Compare to:
const id1 = 1;
// types
type Id = number | string;
type Ids = Id[];
// values
const ids: Id[] = [1, "abc2"];
type Person = {
name: string;
height: number;
};
type Name = Person["name"];
// values
const me: Person = { name: "max", height: 2 };
const myName: string = me.name;
const myFirstName: Person["name"] = me.name;
type Person = {
name: [string, string]
height: number;
};
type Name = Person["name"];
// values
const me: Person = { name: ["max", "heiber"], height: 2 };
const wilson: Person = { name: ["w.", "w.", "wilson" ], height: 2 }; // error
const myFirstName: Person["name"] = me.name;
// definition
type Timed<T> = { value: T; start: Date };
// apply the function
type TimedRoute = Timed<Route>
// type { value: Route; start: Date };
Notice how similar type-level TS is to programming with values:
// type-level
type Timed<T> = { value: T; start: Date };
// valuelevel
const addStartTime = (value) => ({ value, start: new Date() })
type GraphQLPerson = {
name: string;
height: () => number;
};
type UnFunc<T> = { [P in keyof T]: T[P] extends Function ? undefined : T[P] };
type UnFuncyPerson = UnFunc<GraphQLPerson>;
const regularMax: UnFuncyPerson = { name: "max", height: undefined };
type UnFunc<T> = { [P in keyof T]: T[P] extends Function ? undefined : T[P] };
- Not the easiest to read
- But compare to value-level coding!
function unFunc(obj) {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => {
return typeof v === "function" ? [k, undefined] : [k, v];
})
);
}
value-level pattern matching
const obj = { a: 3 };
const { /* infer */ a = undefined } = obj;
a; // 3
type-level pattern matching
// definition
type ParamsOf<T extends Function> = T extends (...args: infer P) => any
? P
: unknown;
// usage
declare function foo(a: string, b: number): void;
type FooParams = ParamsOf<typeof foo>; // type [string, number]
It's like a tiny, pure version of JS!
- with data, functions, and structured programming
- Familiarity with type-level TS as a language
- objects, functions, conditionals, etc. etc.
- Know the Limits
- See beyond the limits
- Know the Limits:
- What is impossible?
- What is awkward?
get
function similar to Lodash.get
:
const person = {
name: 'max'
address: {
street: 'Main St.'
postCode: 06057
}
}
const streetName: string = get(person, 'address', 'street')
- YES
- NO
type-level addition
declare const num: Add<One, Two>;
const two: Two = num; // error
const three: Three = num;
- YES
- NO
type-level addition: YES
type Add<N1 extends Num, N2 extends Num> = {
base: N2;
recur: Add<Minus1<N1>, Plus1<N2>>;
}[N1 extends Zero ? "base" : "recur"];
adapted from microsoft/TypeScript#14833
Can we teach TS to understand the relationship between
- array.length
- array.pop()
const array: number[] = [1, 2, 3]
const f = (n: number) => { }
while (array.length) {
f(array.pop())
// error: 'undefined' is not assignable to 'number'
}
- YES
- NO
const thing = createThing();
thing.doStuff(); // Error: `thing` is not initialized
await thing.init();
thing.doStuff();
- YES
- NO
Mismatch: - Using immutable types to model mutable values`
Solutions:
- this
-aware type narrowing?
Programming in the type system can be awkward! (compared to value-level programming)
type Add<N1 extends Num, N2 extends Num> = {
base: N2;
recur: Add<Minus1<N1>, Plus1<N2>>;
}[N1 extends Zero ? "base" : "recur"];
Imagine your favorite functional programming language MINUS:
- local variables
- functions as parameters
- functions as return values
- with local variables, first-class type-level functions, etc.?
typeFunc Add<N1 extends Num, N2 extends Num> {
if (N1 extends Zero) {
return N2
}
type NewN1 = Minus1<N1>
type NewN2 = Plus1<N2>
return Add<NewN1, NewN2>
}
- with local variables, first-class type-level functions, etc.?
Value-level bind
:
function bind<U extends any[]>(f extends (...origArgs: any[]) => any, ...boundArgs: U) {
return (...args) => f(...boundArgs, ...args)
}
const add1 = bind(add, 1)
Type-level bind
:
typeFunc Bind<F: (<...Any[] => Any) ...BoundArgs: Any[]> {
return <...Args> => F(...BoundArgs, ...Args)
}
type Add1 = Bind<Add, One>
- Type-level programming is just programming
- Most JS code can be decently well-typed
- The occasional awkwardness comes from simplicity, not complexity