Advanced TypeScript
ngconf - March 14 2024
Today!
9:00am - Introductions & set-up
9:30am - TypeScript
10:00am - Types
10:45am - Casting
12:00pm - Lunch 🍽️
11:30pm - Generics
2:00pm - Conditional Types
2:45pm - Mapped Types
4:30pm - Putting it all together!
5:00pm - Finish 🎉
3:30pm - Helper Types
Today!
Intros!
My name is __________
My biggest irrational fear is __________
Today I want to learn __________
Intros!
Intros!
My name is Craig
My irrational fear is cash 🤣
I want to learn all your names
Set-up!
github.com/phenomnomnominal/advanced-typescript
If you don't have access, check your email for instructions.
Go to the repo:
Set-up!
Set-up!
Get the code on your machine:
git clone https://github.com/phenomnomnominal/advanced-typescript.git
Set-up!
Install dependencies:
npm i
Node.js and npm version shouldn't matter, but latest is good!
Set-up!
Check it works:
npm run test
TypeScript!
TypeScript!
Typed super-set of JavaScript - all valid JavaScript code is valid TypeScript
Launched by Microsoft in 2012
Aims to make it easier to write larger JavaScript applications by introducing type-safety.
Has quickly become one of the most-used and most-loved programming languages in the world!
Syntax!
const hello: string = 'world';
let beast: number = 666;
function add (a: number, b: number): number {
return a + b;
}
class Person {
constructor (
private _name: string
) { }
public get name (): string {
return this._name;
}
}
enum Sizes {
SMALL,
MEDIUM,
LARGE
}
Defining values with a type
Defining parameters with types
Defining return type
Defining a class of objects
With typed properties
Defining a enum
Syntax!
interface Set<T> {
/**
* Appends a new element with a specified value to the
* end of the Set.
*/
add(value: T): this;
clear(): void;
/**
* Removes a specified value from the Set.
* @returns Returns true if an element in the Set existed
* and has been removed, or false if the element does not
* exist.
*/
delete(value: T): boolean;
/**
* Executes a provided function once per each value in
* the Set object, in insertion order.
*/
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void;
/**
* @returns a boolean indicating whether an element with
* the specified value exists in the Set or not.
*/
has(value: T): boolean;
/**
* @returns the number of (unique) elements in Set.
*/
readonly size: number;
}
Defining a generic interface for a built-in JavaScript type
Using the generic type
Adding modifiers to properties
Syntax!
type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
Defining a union type for digits
type ParseSeconds<SecondsStr extends string> =
SecondsStr extends Seconds
? SecondsStr
: DateParseError<`ParseSeconds: Invalid Seconds: ${SecondsStr}`>;
type ParseMinutes<MinutesStr extends string> =
MinutesStr extends Minutes
? MinutesStr
: DateParseError<`ParseMinutes: Invalid Minutes: ${MinutesStr}`>;
type ParseHours<HourStr extends string> =
HourStr extends Hours
? HourStr
: DateParseError<`ParseHours: Invalid Hour: ${HourStr}`>;
type Hours = `0${Digits}` | `1${Digits}` | '20' | '21' | '22' | '23' | '24';
type Minutes = `${'0' | '1' | '2' | '3' | '4' | '5'}${Digits}`;
type Seconds = Minutes;
type DateParseError<Message extends string> = { error: Message };
Defining a generic error type
Defining Template Literal types for Hours, Minutes and Seconds
Defining a Conditional Type to parse time strings
Compiler!
// index.js
var hello = 'world';
function add (a, b) {
return a + b;
}
// index.ts
var hello = 'world';
function add (a, b) {
return a + b;
}
tsc
The TypeScript compiler (tsc
) does two main things:
- It statically analyses the code and checks for Type Errors
- It transpiles (converts) TypeScript code into JavaScript code
Type Checking!
// index.ts
const variable = 'whoop!';
console.log(variable * 3);
const variable: "whoop!";
The left-hand side of an arithmetic operation must be of type 'any',
'number', 'bigint', or an enum type.
tsc
TypeScript knows what a const
is so it knows that the variable will always be a string!
tsc
error message - they are often much more convoluted than this!
Type Checking!
// index.ts
const variable: string = null;
console.log(variable.toLowerCase());
tsc
✅
By default, tsc
allows any variable to accept null values!
The type-checker will not report an issue, by there could be runtime errors!
"Billion Dollar Mistake"
Type Checking!
// index.ts
const variable: string = null;
console.log(variable.toLowerCase());
tsc --strict
const variable: string;
Type 'null' is not assignable to type 'string'.
In strict
mode the type-checker will exclude null
as a valid value
Another pretty readable error message!
Type Checking!
// ./src/index.ts
// ./src/types.ts
// third-party-fun
// @types/third-party-fun
// tsconfig.json
tsc --noEmit
✨✨✨✨
// lib.dom.d.ts
Transpiling!
// index.js
tsc
// index.ts
interface Set<T> {
add(value: T): this;
clear(): void;
delete(value: T): boolean;
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void;
has(value: T): boolean;
readonly size: number;
}
type Numbers = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type DateParseError<Message extends string> = { error: Message };
type Hours = `0${Numbers}` | `1${Numbers}` | '20' | '21' | '22' | '23' | '24';
type Minutes = `${'0' | '1' | '2' | '3' | '4' | '5'}${Numbers}`;
type Seconds = Minutes;
Transpiling!
// index.js
var hello = 'world';
function add(a, b) {
return a + b;
}
tsc --target ES5
// index.ts
const hello: string = 'world';
const add = (a: number, b: number): number => {
return a + b;
}
Transpiling!
// index.ts
const hello: string = 'world';
const add = (a: number, b: number): number => {
return a + b;
}
// index.js
const hello = 'world';
const add = (a, b) => {
return a + b;
};
tsc --target ES2015
Transpiling!
// index.ts
async function getFileContents (path: string): Promise<string> {
return await readFile(path, 'utf-8')
}
// index.js
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
function getFileContents(path) {
return __awaiter(this, void 0, void 0, function* () {
return yield readFile(path, 'utf-8');
});
}
tsc --target ES2015
Transpiling!
// index.ts
async function getFileContents (path: string): Promise<string> {
return await readFile(path, 'utf-8')
}
// index.js
async function getFileContents(path) {
return await readFile(path, 'utf-8');
}
tsc --target ES2017
Transpiling!
// index.ts
enum HTTPErrors {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
// index.js
var HTTPErrors;
(function (HTTPErrors) {
HTTPErrors[HTTPErrors["BadRequest"] = 400] = "BadRequest";
HTTPErrors[HTTPErrors["Unauthorized"] = 401] = "Unauthorized";
HTTPErrors[HTTPErrors["Forbidden"] = 403] = "Forbidden";
HTTPErrors[HTTPErrors["NotFound"] = 404] = "NotFound";
})(HTTPErrors || (HTTPErrors = {}));
tsc
Transpiling!
// index.ts
enum HTTPErrors {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
ECMAScript Enum
Transpiling!
// index.js
// index.ts
const enum HTTPErrors {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
tsc
Transpiling!
// index.ts
const enum HTTPErrors {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
console.log(HTTPErrors.BadRequest);
// index.js
console.log(400 /* HTTPErrors.BadRequest */);
tsc
Transpiling!
// index.js
var HTTPErrors;
(function (HTTPErrors) {
HTTPErrors[HTTPErrors["BadRequest"] = 400] = "BadRequest";
HTTPErrors[HTTPErrors["Unauthorized"] = 401] = "Unauthorized";
HTTPErrors[HTTPErrors["Forbidden"] = 403] = "Forbidden";
HTTPErrors[HTTPErrors["NotFound"] = 404] = "NotFound";
})(HTTPErrors || (HTTPErrors = {}));
console.log(HTTPErrors.BadRequest);
babel --presets @babel/preset-typescript
// index.ts
const enum HTTPErrors {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
console.log(HTTPErrors.BadRequest);
Transpiling!
// http-errors.ts
export const enum HTTPErrors {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
// index.ts
import { HTTPErrors } from './http-errors';
console.log(HTTPErrors.BadRequest);
// http-errors.js
// index.js
import { HTTPErrors } from './http-errors';
console.log(HTTPErrors.BadRequest);
babel
babel
Transpiling!
// index.js
console.log(400 /* HTTPErrors.BadRequest */);
tsc
// http-errors.ts
export const enum HTTPErrors {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
// index.ts
import { HTTPErrors } from './http-errors';
console.log(HTTPErrors.BadRequest);
// http-errors.js
Modules!
// index.ts
import { resolve } from 'path';
const CWD = process.cwd();
const SRC = './src';
export const RESOVLED = resolve(CWD, SRC);
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RESOVLED = void 0;
const path_1 = require("path");
const CWD = process.cwd();
const SRC = './src';
exports.RESOVLED = (0, path_1.resolve)(CWD, SRC);
tsc --module CommonJS
define(["require", "exports", "path"], function (require, exports, path_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RESOVLED = void 0;
const CWD = process.cwd();
const SRC = './src';
exports.RESOVLED = (0, path_1.resolve)(CWD, SRC);
});
tsc --module AMD
Modules!
// index.ts
import { resolve } from 'path';
const CWD = process.cwd();
const SRC = './src';
export const RESOVLED = resolve(CWD, SRC);
import { resolve } from 'path';
const CWD = process.cwd();
const SRC = './src';
export const RESOVLED = resolve(CWD, SRC);
tsc --module ESNext
Modules!
// index.ts
import { resolve } from 'path';
const CWD = process.cwd();
const SRC = './src';
export const RESOVLED = resolve(CWD, SRC);
Declaring Types!
type EmailAddress = `${string}@${string}`;
const birthday: Date;
function power (base: number, exponent: number): number {
return base ** exponent;
}
type Location = {
lat: number;
lng: number;
address: string;
}
interface Booking {
date: Date;
location: Location;
}
class MeetingBooking implements Booking {
public constructor (
public date: Date,
public location: Location,
public participants: Array<EmailAddress>
) { }
}
Declaring variables
Declaring functions
Declaring types
Declaring interfaces
Declaring more types
Declaring classes
Declaring Types!
declare namespace global {
interface Window {
mySweetGlobal: string;
}
}
declare module "my-js-lib" {
export function sweetAsyncRandom (): Promise<number>;
}
declare module "*.scss" {
const styles: Record<string, string>;
export default styles;
}
Augmenting the global
namespace with new global variables!
Manually declaring rough types for a third-party module
Manually declaring types for non-JS modules
Exercises!
Types!
Types!
true
123_456_789
'hello world'
[]
{}
null
undefined
Types!
boolean
number
string
Array<T>
Object
null
undefined
true
86_400
'January'
[number, number]
{ id: string }
null
undefined
let name = 'Craig';
typeof name; // string is inferred
Types!
const address: string = null; // ✅ in non-strict mode
const city: string = null; // ❌ in strict mode
const country: string | null = null; // ✅ in stict mode
const age = 33;
typeof age; // 33 is inferred
const isHungry: true = false; // ❌
const isSleepy: boolean = false; // ✅
const location: string = 10000; // ❌
TypeScript uses the typeof
keyword to access the type of a variable!
Types!
undefined
true
86_400
'January'
[number, number]
{ id: string }
null
.valueOf()
.toPrecision(2)
.charAt(0)
[0]
['id']
Primitives!
true
1
'A'
false
1.1
1.1234
-1
NaN
Math.PI
666
"B"
`Cc`
'WOW!'
'🔥'
null
boolean
null
number
string
type Craig = 'Craig';
type ThirtyThree = 33;
type Location = string;
type IsHungry = true;
type IsSleepy = boolean;
Type Declarations!
Objects!
Object
object
boolean
number
string
null
undefined
symbol
const address: Object = null; // ❌
const country: Object = undefined; // ❌
Objects!
Object
const name: Object = 'Craig'; // ✅
const age: Object = 33; // ✅
const location: Object = 10000; // ✅
const isHungry: Object = false; // ✅
const isSleepy: Object = false; // ✅
Objects!
{}
object
Object
Object extends object // true
Object extends {} // true
object extends Object // true
object extends {} // true
{} extends Object // true
{} extends object // true
type Compare = A extends B ? true : false;
Objects!
object
boolean
number
string
null
undefined
symbol
Array<T>
Record<T, T>
{}
Date
RegExp
class Foo
Function
Arrays/Tuples!
[symbol, symbol]
Array<boolean>
bigint[]
[Date]
[number, ...null[]]
Array<T>
[]
Array Opinions!
bigint[]
Array<bigint>
vs
❌
✅
type Phone = { make: string, model: string };
type Phones = Array<Phone>;
🚨
🚨
Tuple Inference!
type Scores = [number, number, number, number];
const SCORES = [100, 200, 300, 400];
typeof SCORES; // Array<number>
const SCORES = [100, 200, 300, 400] as const;
typeof SCORES; // readonly [100, 200, 300, 400]
type Scores = [100, 200, 300, 400];
const assertion
Objects!
Record<string, any>
{ [key: string]: any }
{ id: string }
Record<string, Date>
Index Signature
Objects!
Record<string, any>
{ [key: string]: any }
{ id: string }
let dictA: {
[key: string]: any
} = {
id: '123'
}
let dictB: Record<string, any> = {
id: '456'
}
let dictC: { id: string } = {
id: '789'
}
dictB = dictA; // ✅
dictA = dictC; // ✅
dictC = dictA; // ❌
Object Inference!
const DICT = { id: '789' } as const;
typeof DICT // { readonly id: '789' }
const assertion
const DICT = { id: '789' };
typeof DICT // { id: string }
Modifiers!
type ReadonlyID = {
readonly id: string
}
const readonlyId: ReadonlyID = { id: '123' };
readonlyId.id = '456'; // ❌
type ReadonlyNumbers = ReadonlyArray<number>;
type ReadonlyTuple = readonly [1, 2, 3];
type ReadonlyStrings = readonly string[];
const readonlyNumbers: ReadonlyNumbers = [1, 2, 3];
readonlyNumbers.push(4); // ❌
readonly
modifier
Modifiers!
type OptionalID = {
id?: string
}
const optionalId: OptionalID = { } // ✅
type IDOrUndefined = {
id: string | undefined
}
const noId: IDOrUndefined = { } // ❌
const undefinedId: IDOrUndefined = { id: undefined } // ✅
Optional modifier
Indexes!
type NumberArrayish = {
[index: number]: number
}
const numbers: NumberArrayish = {};
numbers[0] = 0; // ✅
numbers['one'] = 1; // ❌
type Index = NumbersArrayish[0]; // number
type Strings = ReadonlyArray<string>;
type Values = Strings[number]; // string
type Value = Strings[0]; // string
type Length = Strings['length']; // number
const SCORES = [100, 200, 300, 400] as const;
type Scores = typeof SCORES[number]; // 100 | 200 | 300 | 400
type Score = typeof SCORES[0]; // 100
type TupleLength = typeof SCORES['length']; // 4
Another union type!
Indexes!
interface Dictionary {
[key: string]: string;
}
const dict: Dictionary = {};
type Key = Dictionary['name']; // string
interface Dictionary {
[key: string]: string;
size: number; // ❌
}
interface Dictionary {
[key: string]: string | number;
size: number; // ✅
}
Yet another union type!
Indexes!
type Address = {
streetName: string;
streetNumber: number;
suburb: string;
country: string;
postCode: number;
}
// 'streetName' | 'streetNumber' | 'suburb' | 'country' | 'postcode'
type Keys = keyof Address;
const keys = Object.keys(address);
typeof keys; // any
Structural Typing!
type Configuration = {
input: string,
output: string
}
function configure (config: Configuration): void {
// ... handle config
}
configure({
input: './src',
output: './dist',
}); // ✅
configure({
input: './src',
}); // ❌
Functions!
() => void
Function
(a: number) => number
type FuncConstructor = {
new (): Func;
};
class Foo
interface Func {
(): void;
}
🚨 Function Opinions! 🚨
function add (a: number, b: number) {
return a + b;
};
function add (a: number, b: number): number {
return a + b;
};
vs
❌
✅
function add (a: number, b: number): number {
// Type 'string' is not assignable to type 'number' ❌
return a + b + '';
};
Functions!
Function
interface FunctionConstructor {
new(...args: string[]): Function;
(...args: string[]): Function;
readonly prototype: Function;
}
declare var Function: FunctionConstructor;
interface Function {
apply(this: Function, thisArg: any, argArray?: any): any;
call(this: Function, thisArg: any, ...argArray: any[]): any;
bind(this: Function, thisArg: any, ...argArray: any[]): any;
toString(): string;
prototype: any;
readonly length: number;
arguments: any;
caller: Function;
}
this
parameter declaration
Functions!
type Func = (...args: Array<unknown>) => unknown;
type FuncWithThis = (this: unknown, ...args: Array<unknown>) => unknown;
function size (this: Array<unknown>): number {
return this.length;
}
size([]); // ❌
size.call(1); // ❌
size.call([1]); // ✅
Structural Typing!
type EventHandler = (event: Event) => void;
function onEvent (
name: string,
handler: EventHandler
): void {
// ... register handler
}
onEvent('click', () => {
// ... handle click
}); // ✅
onEvent('click', (name: string) => {
// ... handle click
}); // ❌
Structural Typing!
type
value
✅
value
✅
value
❌
const version: number = 1.1; // ✅
const NEGATIVE_BEAST: number = -666; // ✅
const two: 2 = 3; // ❌
const versionStr: string = 1.1; // ❌
const versionObj: object = 1.1; // ❌
const versionObject: Object = 1.1; // ✅
type MapNumber = (a: number) => number;
const noop: MapNumber = () => {}; // ❌
const identity: MapNumber = (n: string) => n; // ❌
const pow: MapNumber = (n: number) => n ** n; // ✅
Structural Typing!
Exercises!
Casting!
const versionStr: string = 1.1; // ❌
const versionObj: object = 1.1; // ❌
const versionObject: Object = 1.1; // ✅
Widening!
type
value
✅
value
✅
value
✅
Widening!
const VERSION = 1.1; // '1.1'
let version = 1.1; // number
Widening!
const VERSION = 1.1; // 1.1'
let version = VERSION; // number
const name: any = 'Craig'; // ✅
const age: any = 33; // ✅
const location: any = 10000; // ✅
const isHungry: any = false; // ✅
const isSleepy: any = false; // ✅
const address: any = null; // ✅
Widening!
Top Types!
null
undefined
any
Object
object
boolean
number
string
symbol
const name: unknown = 'Craig'; // ✅
const age: unknown = 33; // ✅
const location: unknown = 10000; // ✅
const isHungry: unknown = false; // ✅
const isSleepy: unknown = false; // ✅
const address: unknown = null; // ✅
Top Types!
Top Types!
null
undefined
unknown
Object
object
boolean
number
string
symbol
Top Types!
unknown
any
let value: any = 1000;
value.toString(); // Works ✅
value.makeItAMillion(); // Works ✅
let value: unknown = 1000;
value.toString(); // Error ❌
value.makeItAMillion(); // Error ❌
Top Types!
unknown
any
Tells TypeScript that we don't care what type an object is, so tsc
should stop type-checking this variable from here forward.
Tells TypeScript that we don't know what type an object is, so tsc
should make sure we figured out what type it is before we use it.
Casting!
const two: number = 2;
function square(n: number): number {
return n ** 2;
}
// unnecessary but valid - `2` extends `number` ✅
square(two as 2);
// invalid - `number` doesn't extend `object` ❌
square(two as object);
// invalid - `number` extends `Object` but `Object`
// can't be passed to a `number` parameter ❌
square(two as Object);
Casting!
const two: unknown = null;
function square(n: number): number {
return n ** 2;
}
square(two as 2);
unknown
is a top-type, so you can use a Type Assertion to cast it to anything
Casting!
function toArray<T>(value?: ReadonlyArray<T> | Array<T> | T): Array<T> {
if (Array.isArray(value)) {
return value;
}
if (isUndefined(value)) {
return [];
}
return [value as T];
}
try {
// something bad happens:
} catch (error) {
console.log((error as Error)).message;
throw error;
}
Top Types!
any
unknown
Bottom Type!
any
unknown
never
Bottom Type!
const name: never = 'Craig'; // ❌
const age: never = 33; // ❌
const location: never = 10000; // ❌
const isHungry: never = false; // ❌
const isSleepy: never = false; // ❌
const address: never = null; // ❌
Bottom Type!
function throwError (message: string): never {
throw new Error(message);
}
type WithId = { id: string };
type BanId = { id?: never };
function createId<T extends object> (obj: T & BanId): T & WithId {
const needsId: unknown = obj;
(needsId as WithId).id = '';
return needsId as (T & WithId);
}
createId({ }); // Works `id` doesn't exist ✅
createId({ id: '1234 '}); // Error 'string' not assignable to 'never' ❌
Widening!
any
unknown
never
Narrowing!
any
unknown
never
Set Size?
any
never
null
string
boolean
Set Size!
null
null
'January'
3.5
'January'
3.5
Unions!
true
false
boolean
true
false
boolean
type Bool = true | false;
type Bool = boolean;
Unions!
type Num = 1 | 2 | 3 | 4 | ...
type Num = number;
1
2
3
4
number
5
6
7
8
9
10
...
number
1
1
3
4
2
5
6
7
8
9
10
11
12
13
14
...
Unions!
type HTTPError = 400 | 401 | 403 | 404;
const enum HTTPError {
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404
}
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
Narrowing!
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
function isHTTPError (input: unknown): HTTPErrors {
// ???
}
Narrowing!
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
function isHTTPError (input: unknown): HTTPErrors {
if (typeof input === 'number') {
return input;
}
// ???
}
Narrowing!
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
function isHTTPError (input: unknown): HTTPErrors {
if (
input === 400 || input === 401 ||
input === 403 || input === 404
) {
return input;
}
// ???
}
Narrowing!
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
function isHTTPError (input: unknown): HTTPErrors {
if (HTTP_ERRORS.includes(input))) {
return input;
}
// ???
}
Type Guard!
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
function isHTTPError (input: unknown): input is HTTPErrors {
return HTTP_ERRORS.includes(input as HTTPErrors);
}
type predicate!
Type Guard!
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
function isHTTPError (input: unknown): input is HTTPErrors {
return HTTP_ERRORS.includes(input as HTTPErrors);
}
function logHTTPError (input: unknown): void {
if (isHTTPError(input)) {
// typeof input
console.log('Oh no!');
} else {
// typeof input
console.log('Cool.')
}
}
type predicate!
Assertion Guard!
const HTTP_ERRORS = [400, 401, 403, 404] as const;
type HTTPErrors = (typeof HTTP_ERRORS)[number];
function assertHTTPError (input: unknown): asserts input is HTTPErrors {
if (!HTTP_ERRORS.includes(input as HTTPErrors)) {
throw new Error();
}
}
function logHTTPError (input: unknown): void {
assertHTTPError(input);
// typeof input
expensiveLoggingOperation('Oh no!');
}
Intersections!
type WithName = { name: string };
type WithId = { id: string };
type Person = {
dateOfBirth: Date
} & WithName & WithId;
interface Person extends WithName, WithId {
dateOfBirth: Date
};
class Person implements WithName, WithId {
public name: string;
public id: string;
public dateOfBirth: Date;
};
Intersections!
interface Person extends WithName, WithId {
dateOfBirth: Date
};
interface Person {
placeOfBirth: string;
}
// lib.es2015.collection.d.ts
interface Set<T> {
add(value: T): this;
clear(): void;
delete(value: T): boolean;
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void;
has(value: T): boolean;
readonly size: number;
}
// lib.es2015.iterable.d.ts
interface Set<T> {
[Symbol.iterator](): IterableIterator<T>;
entries(): IterableIterator<[T, T]>;
keys(): IterableIterator<T>;
values(): IterableIterator<T>;
}
Template Strings!
type Email = string;
'A'
"B"
`Cc`
'WOW!'
'🔥'
string
Template Strings!
type Email = `${string}@${string}`;
'a@a'
"foo@bar.com"
`🔥@❤️`
Email
Exercises!
Generics!
Generics!
Array<T>
Record<string, any>
Set<T>
Promise<T>
<T>(arg: T) => T
Generics!
function identity (input) {
return input;
}
function identity (input: any): any {
return input;
}
const i = identity('hello');
typeof i; // any
function identity (input: unknown): unknown {
return input;
}
const i = identity('hello');
typeof i; // unknown
Generics!
function identity <T> (input: T): T {
return input;
}
const i = identity('hello');
typeof i; // 'hello'
function identity <Input> (input: Input): Input {
return input;
}
const i = identity<string>('hello'); // i: string
[symbol, symbol]
Array<boolean>
bigint[]
[Date]
[number, ...null[]]
Array<T>
[]
Array<number>
[number]
[1, 2, 3]
Generics!
import { promises as fs } from 'fs'
async function persist <T> (input: T): Promise<T> {
await fs.writeFile('', JSON.stringify(input));
return input;
}
const i = persist('hello'); // i: Promise<string>
import { promises as fs } from 'fs'
async function persist <T> (input: T): Promise<T> {
// Property 'serialise' does not exist on type 'T'
await fs.writeFile('', input.serialise());
return input;
}
Behaves similarly to unknown
Generics!
import { promises as fs } from 'fs'
type Serialisable = {
serialise(): string;
}
async function persist <T extends Serialisable> (input: T): Promise<T> {
await fs.writeFile('', input.serialise());
return input;
}
// Argument of type 'string' is not assignable to parameter of type 'Serialisable'
const error = persist('hello');
const good = persist({ serialise: () => 'hello' });
Generics!
interface Set<T> {
add(value: T): this;
clear(): void;
delete(value: T): boolean;
forEach(
callbackfn: (value: T, value2: T, set: Set<T>) => void,
thisArg?: any
): void;
has(value: T): boolean;
readonly size: number;
}
forEach
on a Set
is a little bit weird, it gets each value passed twice!
Generics!
typefunction Set (Item) {
return {
add(value: Item): this;
clear(): void;
delete(value: Item): boolean;
forEach(/* ... */): void;
has(value: Item): boolean;
readonly size: number;
};
}
type SetNumber = Set<number>;
// {
// add(value: number): this;
// clear(): void;
// delete(value: number): boolean;
// forEach(/* ... */): void;
// has(value: number): boolean;
// readonly size: number;
// }
Generics!
class Runner <RunResult> {
public constructor (private task: () => RunResult) { }
}
const inferredRunner = new Runner(() => 3);
type InferredRunner = typeof inferredRunner; // Runner<number>
class Runner <RunResult extends () => unknown> {
public constructor (private task: RunResult) { }
public run () {
return this.task();
}
}
const badRunner = new Runner(3); // ❌
const goodRunner = new Runner(() => 3); // ✅
type GoodRunner = typeof runner; // Runner<() => 3>
Generics!
function getID <T> (input: T): T['id'] {
return input.id;
}
function getID <T extends { id: unknown }> (input: T): T['id'] {
return input.id;
}
const id = getID({ id: '123' });
This won't work
Template Literal Types!
type USD = `US$${number}`
type Currency<
Prefix extends string,
Suffix extends string = '',
Separator extends string = ',',
Decimal extends string = '.'
> = ...
type USD = Currency<'US$'>;
type Euro = Currency<'€' | '', '€' | '', '.', ','>;
typefunction Currency (
Prefix: string,
Suffix: string = '',
Separator: string = ',',
Decimal: string = '.'
) {
return `${Prefix}...`;
}
Template Literal Types!
type Letter = 'a' | 'b' | 'c' | 'd' | 'e' // ...
type Numbers = 0 | 1 | 2 | 3 | 4 | 5 // ...;
// "a0" | "a4" | "a2" | "a3" | "a1" | "a5" | "a6" | "a7" | "a8" | "a9" |
// "b0" | "b4" | "b2" | "b3" | "b1" | "b5" | ...
type IDs = `${Letter}${Numbers}`;
typefunction Id (
Letters: Array<string>,
Numbers: Array<number>
): Array<string> {
const result = [];
for (let letter in Letters) {
for (let num in Numbers) {
result.push(`${letter}${num}`);
}
}
return result;
}
type HTMLTagsLower = 'a' | 'div' | 'kbd' | 'section' | ...
type HTMLTags = HTMLTagsLower | Uppercase<HTMLTagsLower>;
Intrinsic String Types!
type BlockCaps = Lowercase<string>;
const badPassword: BlockCaps = 'HELLO'; // ❌
const goodPassword: BlockCaps = 'hello'; // ✅
type EventNames = 'click' | 'doubleclick' | 'hover';
// 'onClick' | 'onDoubleclick' | 'onHover';
type EventHandlers = `on${Capitalize<EventNames>}`;
type DotNetType = {
Name: string;
DateOfBirth: Date;
}
// 'name' | 'dateOfBirth';
type JSTypeNames = Uncapitalize<keyof DotNetType>;
Exercises!
Lunch!
Back here ready to go by 1pm!
Conditionals!
Input extends Super ? TrueType : FalseType
typefunction Conditional (
Input,
Super,
TrueType,
FalseType,
) {
if (Input extends Super) {
return TrueType;
}
return FalseType;
}
Conditionals!
Conditionals!
type AssertExtends<
SuperType,
Subtype extends SuperType
> = Subtype extends SuperType ? Subtype : never;
typefunction AssertExtends (
SuperType,
SubType
) {
if (SubType extends SuperType) {
return SubType;
}
return never;
}
Conditionals!
type IsString1<Input> = Input extends string
? Input
: never;
type IsString2<Input extends string> = Input extends string
? Input
: never;
type Str1 = IsString1<false>;
type Str2 = IsString2<false>;
No error - Str1
is never
Error: Type boolean
does not satisfy the constraint string
Conditional Inference!
type Func = (...args: Array<unknown>) => unknown;
type IsFunction<Input> = Input extends Func
? Input
: never;
type ReturnType<Input> =
Input extends (...args: Array<unknown>) => infer Return
? Return
: never;
infer is ✨magic ✨
type FunctionMagic<Input extends Func> =
Input extends (...args: infer Parameters) => infer Return
? [Parameters, Return]
: never;
typefunction FunctionMagic (
Input: Func
) {
if (Input extends Func) {
const Parameters = gimmeParams(Input);
const Return = gimmeReturn(Input);
return [Parameters, Return];
}
return never;
}
Conditional Inference!
Template Inference!
type Domain<Input extends string> =
Input extends `${string}@${infer D}`
? D
: never;
type Gmail = Domain<'craig@gmail.com'>; // gmail.com
type Nope = Domain<'12345'>; // never
type Domain<Input extends string> =
Input extends `${string}@${infer D}`
? D
: { message: `"${Input}" is not an email address`};
type Gmail = Domain<'craig@gmail.com'>; // gmail.com
// { message: "\"12345\" is not an email address" }
type Nope = Domain<'12345'>;
Exercises!
Maps!
Maps!
type Bundles {
'index.js': string,
'1.js': string,
'2.js': string
};
type Keys = keyof Bundles; // 'index.js' | '1.js' | '2.js'
Maps!
type FuncyBundles = {
[Path in keyof Bundles]: () => string
};
// type FuncyBundles = {
// 'index.js': () => string;
// '1.js': () => string;
// '2.js': () => string;
// }
typefunction FuncyBundles () {
const Result = {};
const Paths = keyof Bundles;
Paths.forEach(Path => {
Result[Path] = () => string;
});
return Result;
};
Should probably be a map
or a reduce
, but this seems clearer
Maps!
type LazyBundles = {
[Path in keyof Bundles]: () => Promise<Bundles[Path]>
};
// type LazyBundles = {
// 'index.js': () => Promise<string>;
// '1.js': () => Promise<string>;
// '2.js': () => Promise<string>;
// }
typefunction LazyBundles () {
const Result = {};
const Paths = keyof Bundles;
Paths.forEach(Path => {
Result[Path] = () => Promise<Bundles[Path]>;
});
return Result;
};
Conditional Maps!
type LazyBundlesExceptIndex = {
[Path in keyof Bundles]: Path extends 'index.js'
? Bundles[Path]
: () => Promise<Bundles[Path]>
};
// type LazyBundlesExceptIndex = {
// 'index.js': string;
// '1.js': () => Promise<string>;
// '2.js': () => Promise<string>;
// }
typefunction LazyBundlesExceptIndex () {
const Result = {};
const Paths = keyof Bundles;
Paths.forEach(Path => {
if (Path === 'index.js') {
Result[Path] = Bundles[Path];
} else {
Result[Path] = () => Promise<Bundles[Path]>;
}
});
return Result;
};
Mapped Keys!
type LazyBundles = {
[Path in keyof Bundles as `get${Capitalize<Path>}`]:
() => Promise<Bundles[Path]>
};
// type LazyBundles = {
// "getIndex.js": () => Promise<string>;
// "get1.js": () => Promise<string>;
// "get2.js": () => Promise<string>;
// }
typefunction LazyBundles () {
const Result = {};
const Paths = keyof Bundles;
Paths.forEach(Path => {
Result[`get${Capitalize(Path)}`] = () => Promise<Bundles[Path]>;
});
return Result;
};
Conditional Keys!
type LazyBundles = {
[
Path in keyof Bundles as Path extends 'index.js'
? Path : never
]: () => Promise<Bundles[Path]>
};
// type LazyBundles = {
// "getIndex.js": () => Promise<string>;
// }
typefunction LazyBundles () {
const Result = {};
const Paths = keyof Bundles;
Paths.forEach(Path => {
if (Path === 'index.js') {
Result[Path] = () => Promise<Bundles[Path]>;
}
});
return Result;
};
Mapped Modifiers!
type ConfigInput = {
src?: string;
silent?: boolean;
output?: string;
};
type Required<Input> = {
[Property in keyof Input]-?: Input[Property];
}
type ConfigOutput = Required<ConfigInput>;
// type ConfigInput = {
// src: string;
// silent: boolean;
// output: string;
// };
Kind of weird syntax!
Mapped Modifiers!
type ReadonlyConfig = {
readonly src: string;
readonly silent: boolean;
readonly output: string;
};
type Mutable<Input> = {
-readonly [Property in keyof Input]: Input[Property];
}
type MutableConfig = Mutable<ReadonlyConfig>;
// type ConfigInput = {
// src: string;
// silent: boolean;
// output: string;
// };
Exercises!
Helpers!
Helpers!
type State = {
counter: number;
items: Array<string>;
}
function updateState (state: Partial<State>): State {
return {
...oldState,
...state
};
}
type Partial<T> = unknown;
Helpers!
type ConfigInput = {
src?: string;
silent?: boolean;
output?: string;
};
type ConfigOutput = Required<ConfigInput>;
type Required<T> = unknown;
Helpers!
type ConfigInput = {
src?: string;
silent?: boolean;
output?: string;
};
type ConfigOutput = Required<ConfigInput>;
type ImmutableCounfig = Readonly<ConfigOutput>;
type Readonly<T> = unknown;
Helpers!
type Things = [1, 2, 'hello', null, RegExp];
// 2 | 1
type Numbers = Extract<Things[number], number>;
// RegExp | null | 'hello'
type NonNumbers = Exclude<Things[number], number>;
type Exclude<T, U> = unknown;
type Extract<T, U> = unknown;
Helpers!
type Address = {
streetName: string;
streetNumber: number;
suburb: string;
country: string;
postCode: number;
}
type Addressish = Pick<Address, 'suburb' | 'country'>;
type Addressish2 = Omit<Address, 'streetName' | 'streetNumber' | 'postCode'>;
type Pick<T, K extends keyof T> = unknown;
type Omit<T, K extends keyof any> = unknown;
Helpers!
// string
type JustString = NonNullable<string | null | undefined>;
type NonNullable<T> = unknown;
Helpers!
type ReturnType<Input> = unknown;
type Parameters<Input> = unknown;
function add (a: number, b: number): number {
returb a + b;
}
type Params = Parameters<typeof add>; // [a: number, b: number]
type Result = ReturnType<typeof add>; // number
Helpers!
type ConstructorParameters<
T extends abstract new (...args: any) => any
> = unknown;
class Person {
public constructor (name: string, age: string) { }
}
// Type 'typeof Person' does not satisfy the constraint '(...args: any) => any'
type BadParams = Parameters<typeof Person>; // ❌
// [name: string, age: string]
type GoodParams = ConstructorParameters<typeof Person>;
Helpers!
type InstanceType<
T extends abstract new (...args: any) => any
> = unknown;
class Person {
public constructor (name: string, age: string) { }
}
// Type 'typeof Person' does not satisfy the constraint '(...args: any) => any'
type BadReturnType = ReturnType<typeof Person>; // ❌
// Person
type GoodInstanceType = InstanceType<typeof Person>;
Helpers!
type ThisParameterType<T> = unknown;
function size (this: Array<unknown>): number {
return this.length;
}
type SizeThis = ThisParameterType<typeof size>; // Array<unknown>
Helpers!
interface CallableFunction extends Function {
// ...
bind<T>(this: T, thisArg: ThisParameterType<T>): OmitThisParameter<T>;
}
class Toggle {
constructor(props) {
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({ ... }));
}
render() {
return (
<button onClick={this.handleClick}></button>
);
}
}
type OmitThisParameter<T> = unknown;
Helpers!
type Awaited<T> = unknown;
type AsyncNumber = Promise<number>;
type SyncNumber = Awaited<AsyncNumber>; // number
type ReallySyncNumber = Awaited<Promise<Promise<number>>; // number;
Everything!
Advanced TypeScript Workshop ngconf
By Craig Spence
Advanced TypeScript Workshop ngconf
Advanced TypeScript Workshop - ngconf 2024 Online
- 921