My notes from the Udemy course: Understanding Typescript.
Understanding Typescript
The Typescript Compiler and its Configuration
tsc --init
will initialize the tsconfig.json
file.
Most settings do not matter for a regular user.
lib
lets you include built-in Typescript libraries. E.g. DOM
.
module: "NodeNext"
is a the most modern option for modules.
allowJs: true
lets you mix Javascript code into Typescript projects.
sourceMap: true
is useful when debugging.
noEmitOnError: true
will only output JS files if there are no errors.
tsc --watch
will watch the project and trigger compilation on changes.
npm install @types/<package>
provides types for popular libraries. The supported packages can be found at https://github.com/DefinitelyTyped/DefinitelyTyped.
Next-gen JavaScript and TypeScript
const
is immutable.
let
is mutable.
Unlike var
, const
and let
and block scoped.
Arrow functions can be stored in variables: const double = (a: number) => a * 2;
The splat operator can be used to take “rest parameters”:
const add = (...numbers: number[]): number => {
// return numbers.reduce()
};
The splat operator can also be used to destructure arrays and objects:
const [item1, item2, ...other] = [1,2,3,4,5];
coonst { prop1, prop2 } = { prop1: "foo", prop2: "bar"};
Classes
Classes in TS are not very different than in PHP.
class User {
public readonly username: string;
private _email: string = '';
constructor(username: string, private readonly age: number) {
this.username = username;
}
public get email(): string {
return this._email;
}
public set email(newEmail: string) {
this._email = newEmail;
}
public isMinor(): boolean {
return this.age < 18;
}
static ENTITY = 'user';
static getEntity(): string {
return this.ENTITY;
}
}
// promoted properties
const user = new User('bob', 25);
// setter
user.email = '[email protected]';
// getter
console.log(user.email);
// method using private property
console.log(user.isMinor());
// static property
console.log(User.ENTITY);
// static method
console.log(User.getEntity());
Inheritance is as simple as using extends
:
class SuperUser extends User {
constructor(public role: string) {
// parent constructor
super('admin', 99);
}
}
Contracts can be implemented using interface
or type
definitions.
Using interface
has the advantage that the interface itself can be
extended.
interface Authenticatable {
email: string;
password: string;
login(): void;
logout(): void;
}
type Authenticatable {
email: string;
password: string;
login(): void;
logout(): void;
}
Type aliases have the advantage that you can also use them in type unions.
Type guards can be implemented using type predicates:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
Function overloading lets specify multiple signatures for the same implementation (they must match though):
class Foo {
myMethod(a: string);
myMethod(a: number);
myMethod(a: number, b: string);
myMethod(a: string | number, b?: string) {
alert(a.toString());
}
}
Dynamic object props can be declared with:
type Store = {
[prop: string]: number,
};
let store: Store = {};
store.answer = 42;
It is like a hardcoded record.
as const
can be used to mark a value as deeply readonly.
The satisfies
operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression.
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blau: [0, 0, 255]
// ~~~~ The typo is now caught!
} satisfies Record<Colors, string | RGB>;
Generics
You know what they are about. They are a superior option to any
.
To define a new generic type:
type DataStore<T> = {
[key: string]: T,
};
// using the generic type
let store: DataStore<string> = {};
Functions can also be generic:
function merge<T>(a: T, b: T): T[] {
return [a, b];
}
merge(1, 2); // [1,2]
// also with different types
function merge2<T, U>(a: T, b: U) {
return [a, b];
}
merge2(1, 'Bob'); // [1,'Bob']
You can also add constraints to your type declaration:
// The type used here must be any kind of object, not
// a primitive
function mergeObj<T extends object>(a: T, b: T) {
return { ...a, ...b};
}
You can also specify multiple generic types with different constraints:
function mergeObj<T extends object, U extends object>(a: T, b: U) {
return { ...a, ...b};
}
Classes and interfaces can also be made generic:
class User<T> {
constructor(public id: T) {}
}
interface Role<T> {
}
Deriving Types from Types
You can derive a type from an existing type using typeof
:
type Username = string;
// Use same type for admins. This is **not** the default
// JS typeof operator, but the TF one.
type AdminUsername = typeof Username;
// You can also derive it from an existing structure
const settings = {
difficulty: 'easy',
level: 10,
players: ['John', 'Jane'],
};
type Settings = typeof settings;
// Settings = { difficulty: string; level: number; players: string[] }
// You can also use it on functions
function sum(a: number, b: number) {
return a + b;
}
type SumFn = typeof sum;
// And use it as an argument
function calculate(fn: SumFn) {
//...
}
The keyof
operator can be used to extract the type of
type User = { name: string; age: number };
// UserKeys is an union type of all keys from the object User.
type UserKeys = keyof User;
// T must be an object, and K is a key from this object
function getProp<T extends object, K extends keyof obj>(obj: T, key: K) {
const val = obj[key];
if (vall === undefined || vall === null) {
throw new Error('invalid value');
}
return val;
}
You can use an indexed access type to look up a specific property on another type:
type AppUser = {
name: string,
permissions: {
id: string,
title: string;
}[];
};
type Permissions = AppUser['permissions'];
// You can also get the type from the array values
type Permission = Permissions[number];
Sometimes a type needs to be based on another type. A mapped type is a generic type which uses a union of PropertyKeys (frequently created via a keyof) to iterate through keys to create a type:
type Operations = {
request: (name: string) => void;
cancel: (name: string) => void;
};
type OptionsFlags<T> = {
[Property in keyof T]: boolean;
// Alternate where props are optional
[Property in keyof T]?: boolean;
// Alternate where all props are mandatory, even if they
// are originally optional optional
[Property in keyof T]-?: boolean;
// To make all props readonly
readonly [Property in keyof T]: boolean
// To make all props **not** readonly
-readonly [Property in keyof T]: boolean
};
// E.g. now you can create options for each method of Operations,
// where new constraints will be automatically added.
const opts: OptionsFlags<Operations> = {
request: ['strict', 'dry-run'],
cancel: ['atomic'],
};
Template literal types
type ReadPermissions = 'read' | 'no-read';
type WritePermissions = 'write' | 'no-write';
// This is not a regular template, but a literal template type.
type FilePermissions = `${ReadPermissions}-${WritePermissions}`
// Creates an union type of:
// read-write | read-no-write | no-read-write | no-read-no-write
Conditional types help describe the relation between the types of inputs and outputs.
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
// You can also use constraints
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
// Also as function return types
type FullNamedPerson = { firstName: string; lastName: string };
function getFullName<T extends object>(
person: T
): T extends FullNamedPerson ? string : never {
// check methods and return full name
// return 'fullname';
// or throw an error
throw new Error('error');
}
To describe a function with any amount of parameters:
(...args: any[]) => void
How to infer a type:
type ReturnValueType<T> = T extends (...args: any[]) => infer Ret ? Ret : never;
function add(a: number, b: number) {
return a + b;
}
type AddFn = typeof add;
type AddFnReturnValueType = ReturnValueType<AddFn>;
Built-in utility types:
// https://www.typescriptlang.org/docs/handbook/utility-types.html
Partial<Type>
Required<Type>
Readonly<Type>
Record<Keys, Type>
Pick<Type, Keys>
Omit<Type, Keys>
Exclude<UnionType, ExcludedMembers>
Parameters<Type>
ReturnType<Type>
Decorators
Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. ECMA decorators and TS decorators are slightly different.
export class Post {
@Length(10, 20)
title: string;
}
Decorators can only be used in OOP contexts - classes, methods, properties, setters, and getters.
Creating a custom
// ECMAscript decorator
function logger(target: any, ctx: ClassDecoratorContext) {
console.log('logger decorator');
}
@logger
class Person {
name = 'Max';
greet() {
console.log('Hi, I am ' + this.name);
}
}
To define a type that is a class:
<T extends new (...args: any[]) => any)>
Decorators can be used to modify existing classes:
function logger<T extends new (...args: any[]) => any)>(target: T, ctx: ClassDecoratorContext) {
console.log('logger decorator');
// e.g. override the previous constructor
return class extends target {
constructor(...args: any[]) {
super(...args);
console.log('class constructor');
}
}
}
@logger
class Person {
name = 'Max';
greet() {
console.log('Hi, I am ' + this.name);
}
}
The code inside the decorator is executed when it is attached to its target. The remaining code is executed once the class is instantiated.
You can also decorate methods - in this case, we bind methods to the instance of the class:
function autobind(
target: (...args: any[]) => any, // any method
ctx: ClassMethodDecoratorContext
) {
// It works like a constructor
ctx.addInitializer(function(this: any) {
// Refers to the instantiated class
this[ctx.name] = this[ctx.name].bind(this);
});
// overwrite the original method
return function() {
console.log('add some logging');
target.apply(this);
}
}
class Person {
name = 'Gus';
@autobind
greet() {
console.log('Hi, I am ' + this.name);
}
}
const p = new Person();
const greet = p.greet;
greet(); // works!
Finally, you can also decorate fields:
// target is undefined because the decorate is executed before it
// has been initialized.
function fieldLogger(target: undefined: ctx: ClassFieldDecoratorContext) {
console.log(target);
// You can override the initial value here
return (initialValue: any) => {
console.log(initialValue);
return 'Gustavo';
};
}
class Person {
@fieldLogger
name = 'Gus';
greet() {
console.log('Hi, I am ' + this.name);
}
}
const p = new Person();
p.greet(); // "Hi, I am Gustavo"
A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime.
function replacerDecotaror(
target: (...args: any[]) => any, // any method
ctx: ClassMethodDecoratorContext
) {
// do the decoration
}
function replacer(initialValue: any) {
// Facotory method to create the decorator function.
return function replacerDecoratr(...);
}
Other notes
- Namespaces are deprecated. ES modules are the right way to modularize things.
- Code imported from a module only gets executed once even if imported by different modules.
.d.ts
files contain only type definitions that are not compiled at build time.