This is an interactive presentation. Press "s" to open the speaker view.

10 Things You Did Not Know TypeScript Could Do

Hadrien Milano

Software Engineer, UI

@
 

Focus of this talk

Foundations

Types + JavaScript = TypeScript

JS with types

let someNumber = 1; someNumber = '2';

Type aliases

type MyString = string; const someString: MyString = 'abc';

Object types

type User = { age: number; name: string; isHuman: boolean; };

Operators

Intersection (mixins)

declare function getUser(): any; type User = { username: string; } type Admin = { admin: true } type Moderator = { moderator: true } let john: User & Admin & Moderator = getUser()

Trick #1
Safe html strings.

declare function escapeHtmlString(s: string): string; declare function getUserInput(): string; type SafeString = string & { __isEscaped: true }; function sanitize(raw: string): SafeString { return escapeHtmlString(raw) as SafeString; } function render(template: string, ...args: SafeString[]) { /* ... */ }

Trick #1½

declare function escapeHtmlString(s: string): string; declare function getUserInput(): string; type SafeString = string & { __isEscaped: true }; function sanitize(raw: string): SafeString { return escapeHtmlString(raw) as SafeString; } function html(body: TemplateStringsArray, ...args: string[]): string { let builder = ''; // ... return builder; } const userName = getUserInput(); html`Hello ${userName}!`; // "Hello John!"

Union

type User = { handle: string; }; type Admin = { admin: true }; type Moderator = { modo: true }; declare function getUser(): any; function anyone(who: Admin | User) { /* ... */ } let john: Admin = getUser(); anyone(john);

Trick #2
Type narrowing

interface HumanUser { kind: 'human'; firstname: string; lastname: string; } interface BotUser { kind: 'bot'; handle: string; } type AnyUser = HumanUser | BotUser; function greet(user: AnyUser): string { }

Trick #3
Exhaustive matching

interface HumanUser { type: 'human'; firstname: string; lastname: string; } interface BotUser { type: 'bot'; handle: string; } interface AlienUser { type: 'alien', codename: string } type AnyUser = HumanUser | BotUser | AlienUser; function greet(user: AnyUser): string { switch (user.type) { case 'human': return `Hello ${user.firstname} ${user.lastname}!`; case 'bot': return `0101000110 ${user.handle}`; } }

Trick #3½
Exhaustive matching (without return type)

Bonus trick
Type-level error messages

interface HumanUser { type: 'human'; firstname: string; lastname: string; } interface BotUser { type: 'bot'; handle: string; } interface AlienUser { type: 'alien', codename: string } type AnyUser = HumanUser | BotUser | AlienUser; declare function postMessage(s: string): void; function greet(user: AnyUser): void { switch (user.type) { case 'human': postMessage(`Hello ${user.firstname} ${user.lastname}!`); break; case 'bot': postMessage(`0101000110 ${user.handle}`); break; // Am I missing something? } }

Conditional types


                        A extends B ? Yes : No;
                    

Conditional types

type IsNumber<T> = T extends number ? 'A number' : 'Not a number'; type A = IsNumber<2>; type B = IsNumber<'hello'>;

Conditional type inference

type MyTuple = ['one', 'two']; type T = MyTuple extends [any, infer Second] ? Second : never;

Let's get serious!

HTTP API definition

declare function createClient<T>(api: T): { [k in keyof T]: (params?: any) => Promise<any>}; declare function createServer<T>(api: T, server: { [k in keyof T]?: (req: { params: any }) => void}); // Shared const api = { getAllCats: 'GET /api/cats', getCatByName: 'GET /api/cats/:name' }; // Client const client = createClient(api); // Server const server = createServer(api, {});

Not super TypeScript friendly...

What if...

interface Endpoint<Params extends string> { params: { [k in Params]: string; }; } interface Api { [k: string]: Endpoint<any>; } type HttpClient<T extends Api> = { [k in keyof T]: (params: T[k]['params']) => void; } interface HttpRequest<T extends Endpoint<any>> { params: T['params']; } type HttpServer<T extends Api> = { [k in keyof T]?: (req: HttpRequest<T[k]>) => void; } declare function createClient<T extends Api>(def: T): HttpClient<T>; declare function createServer<T extends Api>(def: T, serv: HttpServer<T>): void; declare function GET(str: TemplateStringsArray): Endpoint<never>; declare function GET<A extends string>(str: TemplateStringsArray, a: A): Endpoint<A>; declare function GET<A extends string, B extends string>(str: TemplateStringsArray, a: A, b: B): Endpoint<A | B>; const api = { getAllCats: GET `/api/cats`, getCatByName: GET `/api/cats/${'name'}` }; const client = createClient(api); const server = createServer(api, { });

Is this... magic?

interface Endpoint<Params extends string> { params: { [k in Params]: string; } } declare function GET<A extends string>(str: TemplateStringsArray, a: A): Endpoint<A>

No, it's TypeScript! (trick #5)

Hmmm...

interface Endpoint<Params extends string> { params: { [k in Params]: string; }; } declare function GET<A extends string>(str: TemplateStringsArray, a: A): Endpoint<A>; declare function GET<A extends string, B extends string>(str: TemplateStringsArray, a: A, b: B): Endpoint<A | B>; declare function GET<A extends string, B extends string, C extends string>(str: TemplateStringsArray, a: A, b: B, c: C): Endpoint<A | B | C>; declare function GET<A extends string, B extends string, C extends string, D extends string>(str: TemplateStringsArray, a: A, b: B, c: C, d: D): Endpoint<A | B | C | D>;

Problem:


                        let Flatten = [A, B, C, ...] -> A | B | C | ...
                    

First attempt

type Flatten<T extends any[]> =

sub-problems:

  • "Rest" of a tuple
  • Recursive types

Rest of a tuple

type Tail<T extends any[]> = ; type test = Tail<['a', 'b', 'c']>

Tuple utilities (trick #4)

type List<Data extends any[]> = (...tail: Data) => void; type Push<El, list extends List<any>> = list extends List<infer Data> ? (head: El, ...tail: Data) => void : never; type Pop<Head, Tail extends any[]> = (h: Head, ...tail: Tail) => void; type Tuple<L extends List<any>> = L extends List<infer T> ? T : never;

Tuple flattening problems

  • "Rest" of a tuple
  • Recursive types

Recursive types

type List<DATA extends any[]> = (...tail: DATA) => void; type Push<El, list extends List<any>> = list extends List<infer Data> ? (head: El, ...tail: Data) => void : never; type Pop<Head, Tail extends any[]> = (h: Head, ...tail: Tail) => void; type Flatten<T extends List<any>> = T extends Pop<infer Head, infer Tail> ? Head | Flatten<List<Tail>> : never type A = Flatten<List<['a', 'b', 'c']>>

Trick #5: Recursive type

type List<DATA extends any[]> = (...tail: DATA) => void; type Push<El, list extends List<any>> = list extends List<infer Data> ? (head: El, ...tail: Data) => void : never; type Pop<Head, Tail extends any[]> = (h: Head, ...tail: Tail) => void; type Flatten<T extends List<any>> = { tail: T extends Pop<infer Head, infer Tail> ? Head | Flatten<List<Tail>> : never; end: never; } [ T extends Pop<unknown, any> ? 'end' : 'tail' ];

Putting it all together

interface Endpoint<Params extends string> { params: { [k in Params]: string; }; } interface Api { [k: string]: Endpoint<any>; } type HttpClient<T extends Api> = { [k in keyof T]: (params: T[k]['params']) => void; } interface HttpRequest<T extends Endpoint<any>> { params: T['params']; } type HttpServer<T extends Api> = { [k in keyof T]?: (req: HttpRequest<T[k]>) => void; } declare function createClient<T extends Api>(def: T): HttpClient<T>; declare function createServer<T extends Api>(def: T, serv: HttpServer<T>): void; type List<DATA extends any[]> = (...tail: DATA) => void; type Push<El, list extends List<any>> = list extends List<infer Data> ? (head: El, ...tail: Data) => void : never; type Pop<Head, Tail extends any[]> = (h: Head, ...tail: Tail) => void; type Flatten<T extends any[]> = FlattenRec<List<T>, 't'>['t']; interface FlattenRec<T extends List<any>, k extends 't'> { t: T extends Pop<infer Head, infer Tail> ? unknown extends Head ? never : Head | FlattenRec<List<Tail>, k>[k] : never; } declare function GET<Args extends string[]>(str: TemplateStringsArray, ...args: Args): Endpoint<Flatten<Args>>; const api = { getAllCats: GET `/api/cats`, getCatByName: GET `/api/cats/${'name'}` }; const client = createClient(api); const server = createServer(api, {});

More at https://github.com/hmil/rest.ts

TypeScript can do a lot...

But can it do everything?

What is everything?

What is everything?

Is TypeScript Turing Complete?

Can it simulate a turing machine?

Can we implement lambda calculus in TypeScript?

[NSFW]

Do not try this at home work.

Variable:
x
var x
Abstraction:
λx. <something>
x => <something>
Application:
<something1> <something2>
<something1>(<something2>)

Example


                          (λx. x) y
                        = y
                    

                          (λx. x x) (λy. y y)
                          (λx=(λy. y y). x x)
                        = (λy. y y) (λy. y y)
                    

Example


                        0 = λf. λx. x
                        1 = λf. λx. f x
                        2 = λf. λx. f (f x)

                        SUCC = λn. λf. λx. f ((n f) x)
                    
SUCC 0 = (λn. λf. λx. f ((n f) x)) 0
SUCC 0 =      λf. λx. f ((0 f) x)
SUCC 0 =      λf. λx. f (((λf. λx. x) f) x)
SUCC 0 =      λf. λx. f ((     λx. x)    x)
SUCC 0 =      λf. λx. f x
SUCC 0 = 1

Basic model


                                ZERO = λf. λx. x
                                ONE = λf. λx. f x
                                TWO = λf. λx. f (f x)
                            
interface Variable<T extends string> { var: T; } interface Application<M, N> { m: M; n: N; } interface Abstraction<Var extends string, Expr> { v: Var; expr: Expr; }

Syntactic sugar


                                ZERO = λf. λx. x
                                ONE = λf. λx. f x
                                TWO = λf. λx. f (f x)
                            
type Variable<T extends string> = T; type Application<M, N> = [M, N]; type Abstraction<Var extends string, Expr> = (l: Var) => Expr;

Reduction step


                        (λx. ... x ...) y    ->     ... y ...
                    
type Variable<T extends string> = T; type Application<M, N> = [M, N]; type Abstraction<Var extends string, Expr> = (l: Var) => Expr; type Reduce<T> = ; type A = (l: 'x') => (l: 'y') => 'x' type B = (l: 'z') => 'z' type test = Reduce<[A, B]>

Let's have fun

Church numerals (integers)

Boolean logic

// Note: This version is slightly optimized in order to hit the recursion limit a bit later type Variable<T extends string> = T; type Application<M, N> = [M, N]; type Abstraction<Var extends string, Expr> = (l: Var) => Expr; type Replace<Expr, Varname, Replacement> = ReplaceRec<Expr, Varname, Replacement, 't'>['t']; interface ReplaceRec<Expr, Varname, Replacement, t extends 't'> { t: Expr extends Variable<infer VarVarname> ? VarVarname extends Varname ? Replacement : Expr : Expr extends Abstraction<infer AbstrVarname, infer AbstrExpr> ? AbstrVarname extends Varname ? Expr : Abstraction<AbstrVarname, ReplaceRec<AbstrExpr, Varname, Replacement, t>[t]> : Expr extends Application<infer Left, infer Right> ? Application<ReplaceRec<Left, Varname, Replacement, t>[t], ReplaceRec<Right, Varname, Replacement, t>[t]> : Expr; } type Reduce<T> = ReduceRec<T, 't'>['t']; interface ReduceRec<T, k extends 't'> { t: T extends Application<infer Left, infer Right> ? Left extends Abstraction<infer Varname, infer Expr> ? Replace<ReduceRec<Expr, k>[k], Varname, ReduceRec<Right, k>[k]> : Application<ReduceRec<Left, k>[k], ReduceRec<Right, k>[k]> : T extends Abstraction<infer Varname, infer Expr> ? Abstraction<Varname, ReduceRec<Expr, k>[k]> : T ; } // Note: The naive implementation of CanReduce hits the recursion limit too fast. // We use this more sofisticated version instead. type CanReduce<T> = CanReduceRec<T, 't'>['t']; interface CanReduceRec<T, k extends 't'> { t: T extends Application<Abstraction<any, any>, any> ? true : T extends Application<infer M, infer N> ? CanReduceRec<M, 't'>[k] | CanReduceRec<N, 't'>[k] : T extends Abstraction<any, infer expr> ? CanReduceRec<expr, 't'>[k] : never; } type ReduceAll<T> = ReduceAllRec<T, 't'>['t']; interface ReduceAllRec<T, k extends 't'> { t: true extends CanReduce<T> ? ReduceAllRec<Reduce<T>, k>[k] : T; } type _0 = (l: 'f') => (l: 'x') => 'x'; type _1 = (l: 'f') => (l: 'x') => ['f', 'x'] type SUCC = (l: 'n') => (l: 'f') => (l: 'x') => ['f',[['n','f'],'x']]; type SUCC_0 = ReduceAll<[SUCC, _0]>;

We've implmented lambda-calculus in TypeScript

Therefore, TypeScript is Turing-Complete.

The end.

or is it?

// Note: This version is slightly optimized in order to hit the recursion limit a bit later type Variable<T extends string> = T; type Application<M, N> = [M, N]; type Abstraction<Var extends string, Expr> = (l: Var) => Expr; type Replace<Expr, Varname, Replacement> = ReplaceRec<Expr, Varname, Replacement, 't'>['t']; interface ReplaceRec<Expr, Varname, Replacement, t extends 't'> { t: Expr extends Variable<infer VarVarname> ? VarVarname extends Varname ? Replacement : Expr : Expr extends Abstraction<infer AbstrVarname, infer AbstrExpr> ? AbstrVarname extends Varname ? Expr : Abstraction<AbstrVarname, ReplaceRec<AbstrExpr, Varname, Replacement, t>[t]> : Expr extends Application<infer Left, infer Right> ? Application<ReplaceRec<Left, Varname, Replacement, t>[t], ReplaceRec<Right, Varname, Replacement, t>[t]> : Expr; } type Reduce<T> = ReduceRec<T, 't'>['t']; interface ReduceRec<T, k extends 't'> { t: T extends Application<infer Left, infer Right> ? Left extends Abstraction<infer Varname, infer Expr> ? Replace<ReduceRec<Expr, k>[k], Varname, ReduceRec<Right, k>[k]> : Application<ReduceRec<Left, k>[k], ReduceRec<Right, k>[k]> : T extends Abstraction<infer Varname, infer Expr> ? Abstraction<Varname, ReduceRec<Expr, k>[k]> : T ; } // Note: The naive implementation of CanReduce hits the recursion limit too fast. // We use this more sofisticated version instead. type CanReduce<T> = CanReduceRec<T, 't'>['t']; interface CanReduceRec<T, k extends 't'> { t: T extends Application<Abstraction<any, any>, any> ? true : T extends Application<infer M, infer N> ? CanReduceRec<M, 't'>[k] | CanReduceRec<N, 't'>[k] : T extends Abstraction<any, infer expr> ? CanReduceRec<expr, 't'>[k] : never; } type ReduceAll<T> = ReduceAllRec<T, 't'>['t']; interface ReduceAllRec<T, k extends 't'> { t: true extends CanReduce<T> ? ReduceAllRec<Reduce<T>, k>[k] : T; } type _4 = (l: 'f') => (l: 'x') => ['f', ['f', ['f', ['f', 'x']]]]; type Mult = (l: 'm') => (l: 'n') => (l: 'f') => ['m', ['n', 'f']]; type _16 = ReduceAll<[[Mult, _4], _4]>;

The end



Stay in touch



Thanks:


B-roll

Raw clips that didn't make it to the presentation.

Conditional type inference distributivity

type Strings = 'a' | 'b' | 'c'; type Test<T> = T extends 'b' ? 'YES' : 'NO';

Decorators


                        @(some expression which evalutates to a function)
                    

class decorator


                        function MyDecorator(t: any) {
                        }

                        @MyDecorator
                        class MyClass {

                        }
                    

or


                        function MyDecorator() {
                            return function(t: any) { }
                        }

                        @MyDecorator()
                        class MyClass {

                        }
                    

class decorator

...or


                        function MyDecoratorOverkillFactory() {
                            return {
                                d: () => {
                                    return function(t: any) { };
                                }
                            };
                        }

                        @MyDecoratorOverkillFactory().d()
                        class MyClass {

                        }
                    

Tip: Restrict the scope of your decorator

abstract class AModel { id: string; } function Model(m: typeof AModel) { } @Model class MyModel extends AModel { }

Member decorator

abstract class AModel { id: string; } function MyDecorator(t: unknown, p: PropertyKey) { } class MyClass extends AModel { @MyDecorator id: string; }

Decorators can't change the type of the object they decorate...

...but they can verify it!

example:
Modelling library

Trick #xxx: Class constraint enforcement from decorator via type-level programing.

function ModelProperty<T>(model: T, key: keyof T) { } class MyModel { @ModelProperty id: string; }

Trick #3
Type guards

interface HumanUser { type: 'human'; firstname: string; lastname: string; } interface BotUser { type: 'bot'; handle: string; } interface AlienUser { type: 'alien', codename: string } type AnyUser = HumanUser | BotUser | AlienUser; function isHuman(t: AnyUser): t is HumanUser { return t.type === 'human'; } function greet(user: AnyUser): string { }