The document discusses advanced TypeScript features including structural typing, type aliases, union and intersection types, and mapped types that allow for the creation of new types based on existing ones. It provides examples of defining and using types, including handling of return types in functions and partial function application with strong typing. The author also emphasizes the utility of TypeScript in maintaining type safety during development with practical coding examples.
Overview of Type Driven Development (TDD) in TypeScript presented by Richard Gibson and Garth Gilmour.
Introduction to TypeScript as a superset of JavaScript, enabling ES2015+ features and type system.
Structural typing emphasizes shape over names in types, illustrated with examples of structurally identical types.
Introduction to complex types, type aliases, and their applications in TypeScript programming.
Union types demonstrate how to define types that can hold multiple types, with practical examples.
Combining multiple types using intersection types, showcasing type definitions for complex structures.
Creating union types from literal values, with examples demonstrating type checks and compiler behavior.
Mapped types for defining new types based on existing structures, including read-only and mutable types.
Strong typing of function return types illustrated through type transformations in a practical coding scenario.
Creating a type mapping for HTML elements, demonstrating strong typing for element creation in TypeScript.
Techniques for extracting function parameter types at compile time, facilitating further type manipulation.Example of typing and implementing partial function application, refining through iterations for accuracy.
Leveraging recursion for compile-time type calculations including list manipulation, length, and head/tail extraction.
Explains the concept of Type Driven Development and its advantages over traditional Test Driven Development.
Additional resources provided for further learning, followed by a question and answer session.
TS is asuperset of JavaScript
• Created by Anders Hejlsberg
Coding in TS enables you to:
• Use the features defined in ES 2015+
• Add types to variables and functions
• Use enums, interfaces, generics etc.
Frameworks like Angular are built on TS
• In particular its support for decorators
• React etc. can benefit from TS
The TypeScript Language
TypeScript
ES6
ES5
– Two typesare identical if they have the same structure
– Even if they are named and defined in unrelated places
– In OO this means the same fields and methods
– This is close to the concept of Duck Typing
– Found in dynamic languages like Python and Ruby
– But the compiler is checking your code at build time
TypeScript is Structural not Nominal
It’s all about the shape of the type...
8.
type Pair ={
first: string,
second: number
};
interface Tuple2 {
first: string;
second: number;
}
class Dyad {
constructor( public first: string,
public second: number) {}
}
An Example of Structural Typing
Three types with
the same shape
9.
function test1(input: Pair){
console.log(`test1 called with ${input.first} and ${input.second}`);
}
function test2(input: Tuple2) {
console.log(`test2 called with ${input.first} and ${input.second}`);
}
function test3(input: Dyad) {
console.log(`test3 called with ${input.first} and ${input.second}`);
}
An Example of Structural Typing
10.
export function structuralTyping(){
let sample1 = { first: "wibble", second: 1234 };
let sample2 = new Dyad("wobble", 5678);
test1(sample1);
test1(sample2);
test2(sample1);
test2(sample2);
test3(sample1);
test3(sample2);
}
An Example of Structural Typing
test1 takes a Pair
test2 takes a Tuple2
test3 takes a Dyad
11.
------ Structural Typing------
test1 called with wibble and 1234
test1 called with wobble and 5678
test2 called with wibble and 1234
test2 called with wobble and 5678
test3 called with wibble and 1234
test3 called with wobble and 5678
12.
– Complex typescan be declared via Type Aliases
– Union Types specify a given type belongs to a set
– Intersection Types combine multiple types together
– String, boolean and number literals can be used as Literal Types
The Weirdness Continues
Some other fun features...
13.
type MyCallback
= (a:string, b:number, c:boolean) => Map<string, boolean>
type MyData = [string, number, boolean];
function sample(func: MyCallback,
data: MyData): Map<string, boolean> {
return func(...data);
}
Type Aliases
these are type aliases
note the spread operator
export function showUnionTypes(){
console.log(demo(40));
console.log(demo(80));
console.log(demo(120));
let result = demo(90);
if(result instanceof Element) {
result.appendChild(document.createTextNode("Hello"));
console.log(result);
}
}
Union Types
no cast required
19.
type Individual ={
think: (topic: string) => string,
feel: (emotion: string) => void
};
type Machine = {
charge: (amount: number) => void,
work: (task: string) => boolean
};
type Cylon = Individual & Machine;
Intersection Types
note the combination
type Homer ="Homer";
type Simpsons = Homer | "Marge" | "Bart" | "Lisa" | "Maggie";
type Flintstones = "Fred" | "Wilma" | "Pebbles";
type Evens = 2 | 4 | 6 | 8 | 10;
type Odds = 1 | 3 | 5 | 7 | 9;
Type Literal Values
assembling types from string literals
assembling types from number literals
22.
function demo1(input: Simpsons| Evens) {
console.log(input);
}
function demo2(input: Flintstones | Odds) {
console.log(input);
}
Type Literal Values
Union Type built from Type Literals
23.
export function showTypeLiterals(){
demo1("Homer");
demo1(6);
//demo1("Betty");
//demo1(7);
demo2("Wilma");
demo2(7);
//demo2("Homer");
//demo2(6);
}
Type Literal Values
this is fine
this is fine
will not compile
will not compile
– Mapped Typeslet us define the shape of a new type
– Based on the structure of one or more existing ones
– You frequently do this with values at runtime
– E.g. staff.map(emp => { emp.salary, emp.dept })
– Consider the ‘three tree problem’
– Where you need three views of the abstraction
– For the UI, Problem Domain and Database
Mapped Types
Defining new types based on old
27.
type InstilReadOnly<T> ={
readonly [K in keyof T]: T[K];
};
type InstilMutable<T> = {
-readonly [K in keyof T]: T[K];
};
type InstilPartial<T> = {
[K in keyof T]?: T[K];
};
type InstilRequired<T> = {
[K in keyof T]-?: T[K];
};
Creating Mapped Types
new type based on T
...but all properties immutable
new type based on T
...but all properties mutable
new type based on T
...but all properties optional
new type based on T
...but all properties mandatory
28.
const person =new Person("Jane", 34, false);
const constantPerson: InstilReadOnly<Person> = person;
const mutablePerson: InstilMutable<Person> = constantPerson;
person.name = "Dave";
//constantPerson.name = "Mary";
mutablePerson.name = "Mary";
Creating Mapped Types
will not compile
fine now
29.
let partialCustomer: InstilPartial<Customer>;
letfullCustomer: InstilRequired<Customer>;
partialCustomer = person;
partialCustomer = {name: "Robin"};
fullCustomer = {
...person,
makeOrder() {}
}
Creating Mapped Types
a person is a partial customer
so is this object literal
we can create a full customer
30.
type Stringify<T> ={
[K in keyof T]: string;
};
type StringifyFields<T> = {
[K in keyof T]: T[K] extends Function ? T[K] : string;
};
Distinguishing Fields From Methods
new type based on T
...but all properties are strings
methods are now excluded
31.
function testStringify() {
constcustomer: Stringify<Customer> = {
name: "Jason",
age: "27",
married: "true",
makeOrder: "whoops"
};
return customer;
}
Distinguishing Fields From Methods
only fields should be strings
– We canalso work with return types at compile time
– Say we have a function which returns an A, B or C
– Where A, B and C are completely unrelated types
– We know we will be testing the type at runtime
– We want the compiler to strongly type the return value
– Based on what is known about the type when we do the return
Compile Space Voodoo Pt.1a
Strongly typing disparate outputs
34.
export function showManagingReturnTypes(){
const data1 = new Centimetres(1000);
const data2 = new Inches(1000);
//input was Centimetres so output is Inches
const result1 = convert(data1).inYards();
//input was Inches so output is Centimetres
const result2 = convert(data2).inMetres();
console.log("1000 centimetres is", result1.toFixed(2), "yards");
console.log("1000 inches is", result2.toFixed(2), "metres");
}
Strongly Typing Outputs
Inches returned
Centimetres
returned
35.
type CentimetresOrInches =Centimetres | Inches;
type CentimetresOrInchesToggle<T extends CentimetresOrInches>
= T extends Centimetres ? Inches : Centimetres;
Strongly Typing Outputs
if T is Inches then CentimetresOrInchesToggle will
be Centimetres (at compile time) and vice versa
our function will return Centimetres or Inches
36.
function convert<T extendsCentimetresOrInches>(input: T):
CentimetresOrInchesToggle<T> {
if (input instanceof Centimetres) {
let inches = new Inches(input.amount / 2.54);
return inches as CentimetresOrInchesToggle<T>;
}
let centimetres = new Centimetres(input.amount * 2.54);
return centimetres as CentimetresOrInchesToggle<T>;
}
Strongly Typing Outputs
WAT?
compiler is certain ‘input’ is Centimetres,
so the return type should be inches
compiler is certain ‘input’ is Inches, so
the return type should be Centimetres
37.
– The lastdemo could have been achieved with overloading
– DOM coding is a more practical example of where it is useful
– A call to ‘document.createElement’ returns a node
– But it would be great to have stricter typing on the result
– So (for example) you could only set ‘source’ on a Video
Compile Space Voodoo Pt.1b
A practical example of typing outputs
38.
type HtmlElements ={
"p": HTMLBodyElement,
"label": HTMLLabelElement,
"canvas": HTMLCanvasElement
}
type ResultElement<T extends string> =
T extends keyof HtmlElements ? HtmlElements[T] : HTMLElement;
Typing HTML Elements
create a type mapping that
associates HTML tags with the
corresponding DOM types
select the correct DOM Node type at compile time
39.
function createElementWithID<T extendsstring>
(name: T, id: string): ResultElement<T> {
const element = document.createElement(name);
element.id = id;
return element as ResultElement<T>;
}
Typing HTML Elements
returning a strongly typed result
40.
export function showTypingHtmlElements(){
const para = createElementWithID("p", "e1");
const label = createElementWithID("label", "e2");
const canvas = createElementWithID("canvas", "e3");
para.innerText = "Paragraphs have content";
label.htmlFor = "other";
canvas.height = 100;
console.log(para);
console.log(label);
console.log(canvas);
}
Typing HTML Elements
Results strongly typed
41.
– We canextract the types of parameters at compile time
– This gets a little weird
– We can't iterate over the names of the parameters
– So we need to resort to techniques like recursive types
Compile Space Voodoo Pt.2
Working with parameters
42.
type AllParams<T extends(...args: any[]) => any> =
T extends ((...args: infer A) =>any) ? A : never;
type FirstParam<T extends any[]> =
T extends [any, ...any[]] ? T[0] : never;
type OtherParams<T extends any[]> =
((...things: T) => any) extends
((first: any, ...others: infer R) => any) ? R : [];
Working With Parameters
WAT?
43.
function demo(p1: string,p2: number, p3: boolean) {
console.log("Demo called with", p1, p2, "and", p3);
}
export function showWorkingWithParameters() {
const var1: AllParams<typeof demo> = ["abc", 123, false];
const var2: FirstParam<AllParams<typeof demo>> = ”def";
const var3: OtherParams<AllParams<typeof demo>> = [456, true];
demo(...var1);
demo(var2, ...var3);
}
Working With Parameters
44.
type AllParams<T extends(...args: any[]) => any> =
T extends ((...args: infer A) =>any) ? A : never;
Working With Parameters
all the parameter types from a function
45.
type FirstParam<T extendsany[]> =
T extends [any, ...any[]] ? T[0] : never;
Working With Parameters
the first parameter type from a function
46.
type OtherParams<T extendsany[]> =
((...things: T) => any) extends
((first: any, ...others: infer R) => any) ? R : [];
Working With Parameters
the other parameter types from a function
47.
– Let’s tryto implement Partial Invocation
– This is where we take an N argument function and produce
– A function that takes a single argument
...which returns a function that takes the other arguments
...which returns the result
Compile Space Voodoo Pt.3
Typing Partial Invocation
WAT?
type AnyFunc =(...args: any[]) => any;
type PartiallyInvoke<T extends AnyFunc> =
T extends ((...args: infer A) => infer R)
? ((first: FirstParam<A>) => (x: OtherParams<AllParams<T>>) => R)
: never;
Typing Partial Invocation
first attempt at a return type, using
‘FirstParam’ and ‘OtherParams’
53.
function partial<T extendsAnyFunc>(func: T): PartiallyInvoke<T> {
return ((first) => (...others) => func(first, ...others))
as PartiallyInvoke<T>;
}
Typing Partial Invocation
standard JavaScript solution,
but with strong typing added
54.
function test1(x: string,y: number, z: boolean): string {
console.log("Demo called with ", x, y, " and ", z);
return "Foobar";
}
function test2(x: number, y: boolean, z: string): number {
console.log("Test 2 called with ", x, y, " and ", z);
return 123;
}
Typing Partial Invocation
55.
export function showPartialApplicationBroken(){
const f1 = partial(test1);
const f2 = partial(test2);
const result1 = f1("abc")([123,true]);
const result2 = f2(123)([false,"abc"]);
console.log(result1);
console.log(result2);
}
Typing Partial Invocation
very close but not quite
... OtherParams produces a tuple
type PartiallyInvoke<T extendsAnyFunc> =
T extends ((...args: infer A) => infer R)
? ((first: FirstParam<A>) => Remainder<T>)
: never;
function partial<T extends AnyFunc>(func: T): PartiallyInvoke<T> {
return ((first) => (...others) => func(first, ...others))
as PartiallyInvoke<T>;
}
Typing Partial Invocation (Iteration 2)
now using a ‘Remainder’ type
61.
type Remainder<T extendsAnyFunc> =
T extends ((...args: infer A) => infer R)
? A extends [infer P1, infer P2]
? (x:P2) => R
: A extends [infer P1, infer P2, infer P3]
? ((x:P2, y:P3) => R)
: A extends [infer P1, infer P2, infer P3, infer P4]
? ((x:P2, y:P3, z:P4) => R)
: never
: never;
Typing Partial Invocation (Iteration 2)
WAT?
62.
type Remainder<T extendsAnyFunc> =
T extends ((...args: infer A) => infer R)
? A extends [infer P1, infer P2]
? (x:P2) => R
Typing Partial Invocation (Iteration 2)
if the original function took two inputs
then disregard the first and use the second
63.
type Remainder<T extendsAnyFunc> =
T extends ((...args: infer A) => infer R)
? A extends [infer P1, infer P2]
? (x:P2) => R
: A extends [infer P1, infer P2, infer P3]
? ((x:P2, y:P3) => R)
Typing Partial Invocation (Iteration 2)
if the original function took three inputs
then disregard the first and use the others
64.
type Remainder<T extendsAnyFunc> =
T extends ((...args: infer A) => infer R)
? A extends [infer P1, infer P2]
? (x:P2) => R
: A extends [infer P1, infer P2, infer P3]
? ((x:P2, y:P3) => R)
: A extends [infer P1, infer P2, infer P3, infer P4]
? ((x:P2, y:P3, z:P4) => R)
Typing Partial Invocation (Iteration 2)
extend as necessary
– What wehave been doing is ‘coding at compile time’
– We have been persuading the TypeScript compiler to create new types and
make inferences for us at compile time
– We have seen that we can make choices
– Via the ternary conditional operator
– There is no support for the procedural loops
– However we can use recursion to iterate at compile time
Compile Space Voodoo Pt.4
Recursive Types
type IncTable ={
0: 1;
1: 2;
2: 3;
3: 4;
4: 5;
5: 6;
6: 7;
7: 8;
8: 9;
9: 10
};
export type Inc<T extends number>
= T extends keyof IncTable ? IncTable[T] : never;
Recursive Types (Numbers)
what do you think Inc<6> would be?
70.
type DecTable ={
10: 9;
9: 8;
8: 7;
7: 6;
6: 5;
5: 4;
4: 3;
3: 2;
2: 1;
1: 0
};
export type Dec<T extends number>
= T extends keyof DecTable ? DecTable[T] : never;
Recursive Types (Numbers)
what do you think Dec<5> would be?
71.
export type Add<Aextends number, B extends number> = {
again: Add<Inc<A>, Dec<B>>
return: A
}[B extends 0 ? "return" : "again"]
Recursive Types (Numbers)
WAT?
recursively increment A whilst also
decrementing B until the latter is 0
type SampleList =[boolean, number, string];
export type Head<T extends any[]> =
T extends [any, ...any[]] ? T[0] : never;
export type Rest<T extends any[]> =
((...t: T) => any) extends ((_: any, ...tail: infer TT) => any)
? TT : []
Recursive Types (Lists)
use the spread operator to
extract the first list item
use the spread operator and function declaration
syntax to extract the remainder of the list
74.
type SampleList =[boolean, number, string];
export type LengthByProperty<T extends any[]> = T['length']
export type LengthByRecursion<T extends any[],
R extends number = 0> = {
again: LengthByRecursion<Rest<T>, Inc<R>>
return: R
}[ T extends [] ? "return" : "again" ]
Recursive Types (Lists)
calculate the length of a list at
compile time in two ways
75.
export type Prepend<E,T extends any[]> =
// assign [E, ...T] to U
((head: E, ...args: T) => any) extends ((...args: infer U) => any)
? U
: never //never reached
Recursive Types (Lists)
use the spread operator and function
declaration syntax to prepend a type
76.
type SampleList =[boolean, number, string];
export type Reverse<T extends any[],
R extends any[] = [],
I extends number = 0> = {
again: Reverse<T, Prepend<T[I], R>, Inc<I>>
return: R
}[ I extends LengthByRecursion<T> ? "return" : "again" ]
Recursive Types (Lists)
– In TestDriven Development we work from the outside in
– The tests cannot write the implementation on our behalf
– But they constrain our choices and point us the right way
– Type Driven Development works the same way
– We are not doing strong typing just to catch errors
– Instead the compiler guides us to the right solution
What is Type Driven Development?
...and why should you try it?