Disclaimer: this is in no way backed by Deepu, just my notes on his talk.
The rise of MV* frameworks
Every day you have a new framework: "yet another framework syndrome". This makes it very difficult to keep up with the trends.
Javascript has the biggest ecosystem.
What is TypeScript
It is a superset of Javascript. Anything valid in Javascript is valid in Typescript and supports the latest features of Javascript (now event to ES10). Typescript is optional.
It is maintained by Microsoft, and there was a big effort by Google as well in the beginning and collaboration is still ongoing.
It comes with a great editor integration.
Transpiling is a pain
So if we have to do it, it is much better to do it with TypeScript, rather than Babel. Because if I'm going to loose that much time transpiling, I might as well have all these for free:
- Type safety
- Great IDE Support
- Easier Refactoring
- Better linting and error identification
- Features from the future
- Much better code style in React and Angular
Feators of TypeScript
- Static type checking, Type inferences, Type aliasing
- Union, intersection, function and hybrid types
- Generics, Decorators (aka Annotations), Mixins (aka Traits)
- Interface, Enum, Tuple support
- Modifiers: Private, Optional, Readonly properties
- JSX support, JS typechecking and more much
Install TypeScript
npm i -g typescript
inits a typescript project with tsconfig.json properties
tsc -init
start watching our project
tsc -w
IMPORTANT: if you set allowJs
(allow js files to be compiled) or checkJs
(report errors in .js
files) and do not specify the outDir
in your tsconfig.json
, then your .js
files will be overridden.
In strict mode
, types like string
do not accept null
as a value and you need to do string | null
.
Interfaces
interface Person {
name: string;
surname: string;
age: number;
sayHi: function;
}
If we compile the interface as is, the above does not produce any code in the js
output, it is used only at the TypeScript level and will check the type of for example:
let john: Person = {
name: "John",
surname: "Di Falco",
age: 12,
sayHi: function() {
console.log(`Hi ${this.name}`);
},
bob: "Hey", // [ts]: Error Object literal may only specify known properties, and 'bob' does not exist in type 'Person'
}
let frank: Person = {}; // [ts]: Error Property 'name', 'surname' ... are missing in type '{}' but required in type 'Person'
So interfaces are TypeScript contracts, restricting to exactly what is specified in the interface.
Access properties: allow you to declare unknown property names and associate them to some type. "You are allowed to add properties of which I do not know the name, as long as their value is of some type".
interface Person {
name: string;
[key: string]: number;
}
Here we allow unknown name properties
Moreover, when you refactor the interface by changing a property, it will automagically rename all objects of that interface type.
You can also have readonly
properties in interfaces. Meaning that they can't be changed after creation.
interface Person {
readonly name: string;
age: number;
readonly sayHi: function;
}
Function interfaces
interface AddFn {
(a: number, b: number): number;
}
const add: AddFn = (a, b) => {
return a + b;
}
Extending interfaces
interface Person {
readonly name: string;
}
interface SuperHero extends Person {
power: string;
applyPower: () => void;
}
It has the effect of adding more properties that require to be implemented AND initialized inside the construtor if not using the shortcut.
class JohnnyBravo implements SuperHero {
readonly name: string;
power: string;
constructor(name: string, power: string) {
this.name = name;
this.power = power;
}
applyPower() {
console.log(`Hoo yeah, where's me comb!?`);
}
}
Constructor initializer shortcut
class JohnnyBravo implements SuperHero {
constructor(readonly name: string, power: string) {}
// constructor has no body, automatically assigns
applyPower() {
console.log(`Hoo yeah, where's me comb!?`);
}
}
Extending classes to turn them into interfaces
interface KoolKid extends JohnnyBravo {
hooYaa: function;
}
Class property Modifiers (private, protected, public, static)
class FatJohnny extends JohnnyBravo {
static unit: string = "kg";
private weight: number;
// cannot use constructor shortcut for subclasses
constructor(name: string, power: string, weight: number) {
super(name, string); // this step is crucial
this.weight = weight
}
protected gun() {
console.log('can be called by subclasses');
}
}
Abstract classes
abstract class Haha {
constructor(bell: string) {}
foo(): string {
return "hey";
}
}
let ha = new Haha("asdf"); // [ts] Cannot create an instance of an abstract class
class Hehe extends Haha {}
let hehe = new Hehe("asdf"); // ok
class Hahaha extends Haha {
bar() {
console.log('bar tender');
}
}
let haha: Haha = new Hahaha("asdf"); // [ts] type Haha
haha.bar(); // [ts] Error: property 'bar' does not exist on type Haha
let hahaha = new Hahaha("asdf"); // [ts] type Hahaha
hahaha.foo(); //ok
hahaha.bar(); //ok
Function overloading
function add(x: string, y: string): string;
function add(x: number, y: number): number;
function add(x: any, y: any): any {
if (typeof x === 'string') {
return `${x} + ${y} = ${add(parseInt(x), parseInt(y))}`;
} else if (typeof x === 'number') {
return x + y
}
throw new Error('Never reached but not detected by ts');
}
add("1", "2");
add([], []); // [ts] Error! No overload matches this call.
// Overload 1 of 2, '(x: string, y: string): string', gave the following error.
// Argument of type 'never[]' is not assignable to parameter of type 'string'.
// Overload 2 of 2, '(x: number, y: number): number', gave the following error.
// Argument of type 'never[]' is not assignable to parameter of type 'number'.
The throw Error
above is never thrown because the function definition is not part of the overlaod. Meaning that it can only be called with two sets of paramters: string
or number
.
Generics
class Greeter<T> {
private greeting: T;
constructor(message: T) {
this.greeting = message;
}
public greet() {
return `Hello ${this.greeting}`;
}
}
const greeter = new Greeter<string>("World");
const greeter2 = new Greeter<number>(10);
Generics restriction (narrowing)
We can also use generics but restrict them to some type:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // ok to use that since T extends Lengthwise
return arg;
}
const lw = {
randomStuff: 2,
length: 1,
}
const nolen = {
randomStuff: 2,
}
loggingIdentity<typeof lw>(lw);
loggingIdentity<typeof lw>(nolen); // [ts] Error: Argument of type '{ randomStuff: number; }' is not assignable to parameter of type '{ randomStuff: number; length: number; }'.
loggingIdentity<typeof nolen>(nolen); // [ts] Error: Type '{ randomStuff: number; }' does not satisfy the constraint 'Lengthwise'.
Dead code removal
Tyescript removes code that is unreachable when it compiles js.
Namespaces
Namespaces allow you to modularize in a sort of sandbox your code. Useful for library writers etc.
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class lettersOnlyValidator implements StringValidator {
isAcceptable(s: string): boolean {
return lettersRegexp.test(s);
}
}
export class ZipValidator implements StringValidator {
isAcceptable(s: string): boolean {
return numberRegexp.test(s);
}
}
}
const validator = new Validation.lettersOnlyValidator()
validator.isAcceptable("asdf0");
Decorators
You can decorate:
- Methods
- Classes
- Properties
- Paramters
- Accessor
[Decorators][decorators-ts] are considered experimental even though they have been there for a while. To enable them you need to "experimentalDecorators": true
in your tsconfig.json
:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Note: property descriptors are a Javascript thing with the following API:
const propertyDescriptor = {
configurable: false, // true <=> type of this prop descr. may be changed and if the prop may be deleted from the corresponding object
enumerable: false, // true <=> property shows up during enumeration of properties on the corresponding object
// only for data descriptors optional keys
value: undefined, // value associated with the property. Can be any valid javascript
writable: false, // true <=> associated with the property may be changed with an assignement operator
// accessor descriptors also have these optional keys
get: undefined, // a function which serves as a getter for the property, when the property is accessed this function is called with no arguements.
set: undefined, // a function chich serves as a setter for the property
}
// using the descriptor on an empty object to define a key "myKey"
let obj = {};
Object.defineProperty(obj, 'myKey', propertyDescriptor);
obj.myKey;
Here you see that there is a descriptor as third parameter, which is passed to the decorator with the decorated prop's descriptor.
// direct decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function() {
console.log(`decorated ${propertyKey}, the method code is: `, originalMethod);
originalMethod();
}
}
//decorator factory
function logFactory(someParam: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`decorated ${propertyKey}, the method code is: `, originalMethod, someParam);
originalMethod.apply(this, args);
}
}
}
class Foo {
@log
foo() {
console.log("Hello there!");
}
@logFactory("a config")
boo() {
}
}
const p = new Foo();
p.foo();
Decorators basically transpile to javascript code that uses Reflection
a lot. It is an API that allows inspection.
IMPORTANT: if the decorator method returns a value, it is used as the Property Descriptor.
IMPORTANT: decorators can be composed like in maths
function f() {
console.log("f()")
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let method = descriptor.value;
descriptor.value = function(...args) {
console.log("f:addition called\n");
method.apply(this, args)
}
}
}
function g() {
console.log("g()")
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let method = descriptor.value;
descriptor.value = function(...args) {
console.log("g:addition called\n");
method.apply(this, args)
}
}
}
class C {
@f()
@g()
foo() {
console.log("foo called");
}
}
let c = new C;
c.foo();
// f()
// g()
// g:addition called
// f:addition called
// foo called
See the order of calling is like in maths. Using the decorator factory makes initial factory calls which return the decorators themselves. Then the actual decorators get called, composing from the last the first: f o g o foo
.
Decorator Factory
Another example using the decorator factory in order to toggle the desired decorator behavior:
class Greeter {
constructor(greeting: string) {}
@enumerable(false)
greet() {
return `Hello ${this.greeting}`;
}
}
function enumerable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
}
}
Class Decorators
The class decorator is declared just before the class declaration.
The class decorator is applied to the constructor of the class and can be used to observe, modify or replace a class definition. A class decorator cannot be used in declaration file, or in any other ambient context (such as on a declare
class).
The expression for the class decorator will be called as a function at runtime, with the constructor of the decorated class as its only argument.
If the class decorator returns a value, it will replace the class declaration with the provided constructor function.
NOTE: SHould you choose to return a new constructor function, you must take care to maintain the original prototype. The logic that applies decorators at runtime will not do this for you.
Let's for example seal an object, preventing new properties from being added to it and making all existing properties non-configurable.
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return `Hello ${this.greeting}!`;
}
}
function sealed(constructor: Fuction) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
This effectively seals the constructor and it's prototype. Meaning that we cannot add anything or edit anything in the class.
IMPORTANT: take good notice on how we need to Object.seal
the prototype as well.
How to override the constructor:
function classDecorator<T extends {new(...args: any[]):()}>(constructor: T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}
@classDecorator
class Greeter {
property = "property";
hello: string:
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"))
Accessor Decorator
Note: typescript disalows decorators on both get
and set
accessors. This is because the decorators are applied to the Property Descriptor and it contains both the get
and set
.
class Point {
private _x: number;
constructor(x: number) {
this._x = x;
}
@configurable(false)
get x() { return this._x; }
}
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
}
}
Property & Paramter decorator
[Read this post][reflection-metadata]. There is a reflection metadata library for typscript that can be installed using.
npm i reflect-metadata
Constructor Type
class Animal {}
class Penguin extends Animal {}
class Zebra extends Animal {
a: string;
constructor(a: string) {
super();
this.a = a;
}
}
class Zoo<T extends Animal> {
constructor(public readonly AnimalClass: new() => T) {}
}
// or different notation
class Zooo<T extends Animal> {
readonly AnimalClass: new() => T;
constructor(animalClass: new() => T) {
this.AnimalClass = animalClass;
}
}
// or allowing a constructor with many arguments
class Zooo<T extends Animal> {
constructor(public readonly AnimalClass: new(...args: any[]) => T) {}
}
const penguinZoo = new Zoo(Penguin);
const zooPenguin = new penguinZoo.AnimalClass()
const zebraZoo = new Zooo(Zebra);
const zooZebra = new zebraZoo.AnimalClass("zebrilla");
zooZebra.a
Intersection and Union types
An intersection type combines multiple types into one: Person & Wolf
. Only values that conform the union of properties. Think of it as different light colors adding up and intersecting creating a white light. So any light having that white color will match the type intersection.
interface Runner {
speed: number;
run(distance: number): number;
}
interface Swimmer {
speed: number;
swim(distance: number): number;
}
const p1: Runner & Swimmer = {
speed: 0,
swim: (a: number) => a,
run: (a: number) => a,
}
If you try to create an intersection from two primitive types, you will end up with type never:
type myNeverType = number & string; // never
Since there is no value that will ever be number
and string
, the type formed by the intersection above is actually never
.
A union type allows you to have either one or the other or both: string | number
. This is very useful for allowing different types in a parameter for example.
const n1: Runner | Swimmer = { // ok
speed: 1,
swim: (a: number) => a,
};
const n2: Runner | Swimmer = { // ok
speed: 1,
run: (a: number) => a,
};
const n3: Runner | Swimmer = { // ok
speed: 1,
run: (a: number) => a,
swim: (a: number) => a,
};
const n4: Runner | Swimmer = { // [ts] Error: Type '{ speed: number; }' is not assignable to type 'Runner | Swimmer', Property 'run' is missing
speed: 1,
};
But it is important to note that on a value that is of a union type, you can only access members that are common to each of the inner types in the union.
n3.swim; // [ts] ok
n3.run(10); // [ts] Error, run is not guaranteed to be available
n2.swim(10) // [ts] Error : [js] Access of undefined
Note that in the example above, n3
should be defined as an intersection rather than a union, since we implement all properties. This would allow us to call n3.run(10)
or n3.swim(10)
Type Guads
Union types are useful to model situations where values can be of types several types. But when these types do not share any member, we cannot access any member of the union type value. How then can we tell which of the types within the union, a union type value has? For example if we have the union type: Dog | Tortoise | Fish
how can we tell if we have a Fish
specifically? Using type guards.
First see an example of failing code without any check:
const n1: Runner | Swimmer = { // ok
speed: 1,
swim: (a: number) => a,
};
if (n1.swim) { // [ts] Error
n1.swim(10);
} else if (n1.run) { // [ts] Error
n1.run(10);
}
To still be able to check the presence of a specific member that is not shared by all the subtypes in the union type, we use type assertions like this:
if ((n1 as Swimmer).swim) { // ok
(n1 as Swimmer).swim(10);
} else if ((n1 as Runner).run) { // ok
(n1 as Swimmer).run(10);
}
User-defined Type Guards
There is much repetition in the code above. Is there a way to test once and assume afterwards? Instead of type assertions we need to use type guards.
Type Guard: some expression that performs runtime check that guarantees the type in some scope.
Type Guards with Type Predicates
To define a type guard, we simply need to define a function whose return type is a type predicate: paramName is Type
like in the follwing example:
function isRunner(athlete: Runner | Swimmer): athlete is Runner {
return (athlete as Runner).run !== undefined;
}
const myAthlete: Runner | Swimmer = { // ok
speed: 1,
swim: (a: number) => a,
};
// BOTH calls to 'run' and 'swim' are now ok
if (isRunner(myAthlete)) {
myAthlete.run(10);
} else { // if we dont have a Runner it must be a Swimmer
myAthlete.swim(10);
}
the athelete is Runner
is our type predicate in the example above.
Type Guards with the in
operator
The in
operator now acts as a narrowing expression for types.
For a n in x
expression, where n
is a string literal or string literal type and x
is a union type, the "true" branch narrows to types which have an optional or required property n
, and the "false" branch narrows to types which have an optional or missing property n
.
function move(athlete: Runner | Swimmer) {
if ("swim" in athlete) {
return athlete.swim(10);
}
return athlete.run(10)
}
Type Guards with typeof
When you are dealing with unions of primitive types, it is kind of a pain to define type predicate functions for them. Especially when in javascript we could use typeof x === 'number'
.
Luckily, typescript will recognise typeof x === 'typename'
as a type guard on its own (typename
must be one of: number
, string
, boolean
, or symbol
). So this would be allowed as well:
const a2: string | number = (function (): string | number {
return (0.5 > Math.random())? "" : 1;
})();
if (typeof a2 === 'number') {
a2.toFixed();
} else {
a2.toLowerCase();
}
Type Guards with instanceof
Similar to how typeof
works on primitive types, instanceof
narrows down types using their constructor function.
if (hey instanceof SomeHeyClass) {
hey.someHeyMethod(); // type of hey narrowed to SomeHeyClass
}
The right hand side of instanceof
needs to be a constructor function, and typescript will narrow it down to:
- the type of the function's
prototype
property if it is notany
- the union of types returned by that type's construct signatures
Nullable types
TypeScript has two special types, null
and undefined
, that have the value null
and undefined
respectively. By default, the type checker considers null
and undefined
assignable to anything (so they are valid values for every type). This means it is not possible to stop them from being assigned to any type: "billion dollar mistake".
--strictNullChecks
flag fixes this; when you declare a variable, it doesn;t automatically include null
or undefined
. But then you can include them explicitely with a union type:
let sn: string | null = "bar"
sn = null; // [ts] ok
Optional parameter and properties
With --strictNullChecks
, optional parameters and properties automatically get | undefined
added to the specified type:
const a = function same(b?: string): string {
return b || "":
}
// without strict null checks
same(null); // [ts] ok
// with strict null checks
same(null); // [ts] Error - null is not assignable to string | undefined
or with properties
class C {
a?: number;
}
let c = new C();
// with strict null checks
c.a = null; // [ts] Error - null is not assignable to string | undefined
Removing null
or undefined
We can remove null
with if
and else
branches testing for null
or with the terser operation:
function hey(name: string | null): string {
return name || "Bob";
}
function ho(name: string | null): string {
if (name == null) {
return "default";
} else {
return name;
}
}
Postfix !
When you know that the value will not be null
or undefined
but the compiler does not allow you to make that assumption because of the context (for example a nested function), then you can use someVar!
the !
tells typescript that someVar
is not going to be null
or undefined
.
So if someVar
was of type string | null
, then it will allow you to call someVar!.charAt(0)
because since it is not null
, it must be string
.
let a: string | null;
a = null;
a.charAt(0); // [ts] Error - Object is possibly 'null'
a!.charAt(0); // [ts] Ok ; [js] - Error Uncaught TypeError: Cannot read property 'charAt' of null
But of course you would not want to do that. The proper thing here is to rely on typescript's error, and pass a string to a
instead of null
. Otherwise crashes in js.
But there are times when typescript does not know what the actual value of a variable is at compile time. Even if you have taken precautions (1) to make sure it is never null
like in the following example:
function broken(name: string | null): string {
function nested(): string {
return name.charAt(0); // [ts] Error - Object is possibly 'null'
}
name = name || "Bob"; // (1) Make sure name is always string
return nested();
}
In the above example, TypeScript is incapable of knowing the value of name
within the nested function nested
, even though we set it to "Bob"
when null
in (1).
IMPORTANT: since at this point we know better what the value of name
will be withing the nested function because of the precation (1), we can tell typescript to remove the null
type from the name
variable with the postfix !
.
function solved(name: string | null): string {
function nested(): string {
return name!.charAt(0); // [ts] - Ok, we tell ts that name does not contain null, so it contains only string
}
name = name || "Bob";
return nested();
}
Note: it has nothing to do with the fact that we took the precaution in (1) that makes typescript allow us to use !
. The usage of !
is allowed anywhere and it will make typescript remove the possible null
value from the var, regardless of whether it is null or not. So we are smarting out typescript.
IMPORTANT: whenever you write a union type, also write a typeguard
Optional Chaning : Postfix ?.
Optional property access lets you tell TypeScript to access a property unless it is null
or undefined
, in which case TypeScript should stop and return undefined
.
let x = foo?.bar.baz();
Above if foo === undefined || foo === null
then x
will be undefined
otherwise it will receive foo.bar.baz()
. It's equivalent to:
let x = (foo === undefined || foo === null)
? undefined
: foo.bar.baz();
And to check if baz
is also present we would do:
let x = foo?.bar?.baz.?();
// replaces
let x;
if (foo && fo.bar && foo.bar.baz) {
x = foo.bar.baz();
}
Keep in mind that ?.
acts differently than &&
since &&
will exit for any falsey value like 0
, NaN
, ""
and false
. It is an intentional feature, which should be used to test whether the property "exists".
Optional element access acts similarly to optional property access, but it allows us to access non-identifier properties (e.g. arbitrary strings, numbers, and symbols):
// get the first element of an array if we have an array,
// otherwise return undefined
function tryGetFirstElement<T>(arr?: T[]) {
return arr?.[0];
// equivalent to
// return (arr === null || arr === undefined)? undeinfed: arr[0]
}
Note: In javascript accessing a nonexistent key of an array returns undefined
but no error. So that is why we do not test for the length of the array.
Optional Call allows us to conditionally call expressions if they're not noll
or undefined
.
async function makeRequest(url: string, log?: (msg: string) => void) {
log?.(`Request started at ${new Date().toISOString()}`);
}
Important: the ?.
magic stops at property access level, so if it is part of a larger expression, the rest of the expression will still be executed and have to deal with an undefined
in case of null
or undefined
.
let res = foo?.bar / someComputation();
// is equivalent to
let temp = (foo === undefined || foo === null)
Nullish Coalescing: ??
The nullish coalescing operator : ??
is a way to "fall back" to a default value when dealing with null
or undefined
:
let x = foo ?? bar();
// equivalent to
let x = (foo !== null && foo !== undefined)
? foo
: bar();
It can replace uses of ||
, which will disregard falsey values, whereas ??
will evaluate to the right hand side part only with null
or undefined
.
let volume = localStorage.volume || 0.5; // if ls.volume is 0, volume will be set to 0.5
// with
let volume = localStorage.volumen ?? 0.5; // if ls.volume is 0, volume will be set to 0
- The right argument of
??
is evaluated only if needed ("short circuiting"). ??
has lower precedence than||
.??
cannot immediately contain, or be contained within, an&&
or||
operation.- The right argument is selected if the left argument is
null
orundefined
.
Type Aliases
Type aliases create a new name for a type. Type aliases are almost like interfaces. Whaever can defined with an interface you can also defined as a type. The only difference, is that with type, you can also alias primitives, and tuples:
type GreeterAlias = Greeter;
type StringTuple2 = [string, string];
Type aliases are nice to keep your code clean, for example instead of writing:
function (myAnimal: (Bird & Reptile) | Insect): (Bird & Reptile) | Insect {
// some code herec
}
you can write:
// Bird and reptile intersection (both at the same time)
type Dragon = Bird & Reptile;
// Dragon and Insect union (either is ok)
type DangerousAnimal = Dragon | Insect;
// In a sigle statement
// type DangerousAnimal = (Bird & Reptile) | Insect;
function (myAnimal: DangerousAnimal): DangerousAnimal {
// some code here
}
Just like interfaces, aliases can also be generic - we can add type paramters like so:
type Container<T> = { value: T };
We can also have a type alias refer to itself in a property:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
Together with intersection types we can create some pretty mind blowing types:
type LinkedList<T> = T & { next: LinkedList<T> | null };
interface Person {
name: string;
}
let people: LinkedList<Person>;
people = { name: "John", next: null };
people.next = people;
people.next.next.next.next.name; // [js] "John"
// or
people = { name: "John", next: null };
people.next = { ...people };
people.next.name; // [js] "John"
Interfaces vs. Type Aliases
Type aliases act sort of like interfaces except in some subtle cases:
One main difference is that interfaces create a new name. Type aliases don't create a new name - for instance, error messages won't use the alias name. Hovering over aliased
, will show an object literal as return type, whereas interfaced
will show Interface
as return type (not the case when I tested).
type Alias = { num: number };
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
Important: an ideal property of software is being open to extension, you should always use an intefface
over a type
alias if possible. On the other hand if you cannot express some shape with an interface
and you need to use a union
or tuple
type, then type
aliases are usually the way to go.
String Literal Types
String literal types allow you to specify the exact value a string must have. In practice, string literal types combine nicely with union types, type guards, and type aliases.
You can use these features together to get enum-like behavior with strings:
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIELement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
} else if (easing === "ease-out") {
} else if (easing === "ease-in-out") {
} else {
// Code never goes here if strictNullChecks is on, but since we did not use a switch statement on the easing param; TypeScript is not aware of that the code will never go here, so it does not complain.
}
}
}
This is really handy to get some intellisense on specific api's and make sure that you only pass certain values to it.
Note: The above code can be rewritten such that typescript complains if not all the cases are handled in the implementation, one is using a switch
and a return
type for animate
, the other option is to use the assertNaver
pattern like this:
function assertNaver(x: never): never {
throw new Error("Unexpected object: " + x);
}
class UIELement1 {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
return "";
} else if (easing === "ease-out") {
return "";
} else {
return assertNaver(easing); // [ts] Error Argument of type '"ease-in-out"' is not assignable to parameter of type 'never'
// ~~~~~~
}
}
}
String literal types can also be used to distinguish overloads:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overlaods
function createElement(tagName: string): Element {
// ... some code
}
Numeric Literal Types
type DiceOutcome = 1 | 2 | 3 | 4 | 5 | 6;
function rollDice(): DiceOutcome {
return Math.ceil(Math.random() * 6) as DiceOutcome;
}
They are seldom written explicitely, but they can be useful narrowing issues and catch bugs.
function foo(x: number) {
if (x !== 1 || x !== 2) { // [ts] Error Operator !== cannot be applied to types '1' and '2'
// ~~~~~~~
}
}
Above after the first test of inequality, we knwow that x === 1
, therefore TypeScript is telling us that the second part || x !== 2
is useless (and a bug).
Enum Member Types
When all members in an enum have literal enum values, some special semantics come into play:
-
enum members become types and we can say that certain members can only have the value of an enum member:
enum ShapeKind { Circle, Square, } interface Circle { kind: ShapeKind.Circle; radius: number; } let c: Circle = { kind: ShapeKid.Square, // [ts] Error: Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle' radius: 100, }
-
Enum types themselves effectively become a union of each enum member. Meaning that the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself. Thanks to that, Typescript can catch silly bugs where we might be comparing values incorrectly:
function f(shape: ShapeKind) { if (shape !== ShapeKind.Circle || shape !== ShapeKind.Square) { // [ts] Error Operator !== cannot be applied to types 'ShapeKind.Circle' and 'ShapeKind.Square' } }
This one is the same as the test with
1 !== x || 2 !== x
, typescript knows that whenx
is1
it will always be different than2
. Similarly it knows thatShapeKind.Circle
will allways be different thanShapeKind.Square
since they are different members of the same enum.
Discriminated Unions
Using a combination of singleton types, union types, type guards, and type aliases we can build an advanced pattern called Discriminated Unions aka tagged unions or algebraic data types. Discriminated unions are useful in functional programming.
- Types that have a common, singleton type property - the discriminant
- A type alias that takes the union of those types - the union
- Type guards on the common property
interface Square {
kind: "square"; //discriminant
size: number;
}
interface Rectangle {
kind: "rectangle"; //discriminant
width: number;
height: number;
}
interface Circle {
kind: "circle"; //discriminant
radius: number;
}
// now we union them into a common theme
type Shape = Square | Rectangle | Circle; // all have kind prop which is called the discriminant
// and we use the discriminated union
function area(s: Shape) {
switch(s.kind) { // we discriminate using the discriminant prop
case "sqaure": return s.size ** 2;
case "rectangle": return s.height * s.width;
case "circle": return 2 * Math.PI *s.radius ** 2;
}
}
Polymorphic this
A polymorphic this
type represents a type that is the subtype of the containing class or interface. This is called F-bounded polymorphism. This makes hierarchi fluent interfaces much easier to express.
class BasicCalc {
constructor(protected value: number = 0) {}
add(x: number): this {
this.value += x;
return this;
}
ans() {
return this.value;
}
}
class ScientificCalc extends BasicCalc {
sin(): this {
Math.sin(this.value);
return this;
}
}
let v = new ScientificCalc()
.add(10)
.sin();
Since the class uses this
types, you can extend it and the new class can use the old methods with no changes.
Note: when should super
be used in a constructor? Whenever we extend the superclass, and the extending class overrides some method (notable the constructor), then need to call super(params)
within the extending's class constructor. Similarly for methods we want to override (and reuse the parent behavior).
Index Types
With index types, the compiler can check code that uses dynamic property names. For example a common pattern is to pick a subset of properties from an object:
function pluck(o, propNames) {
return propNames.map(k => o[k]);
}
In order to implement this in TypeScript we need to use Index type Query and Indexed access operators:
function pluck<T, K extends keyof T>(o: T, propNames: K[]): T[K][] {
return propNames.map(k => o[k]);
}
interface Car {
manufacturer: string;
model: string;
year: number;
}
let taxi: Car = {
manucaturer: "Toyota",
model: "Prius",
year: 2014,
};
let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);
let modelYear: (string | number)[] = pluck(taxi, ["model", "year"]);
The compiler checks that manufacturer
and model
are actually properties on Car
. The example introduces a couple of new type operators:
-
keyof T
is the index type query operator. For any typeT
,keyof T
is the union of known, public property names ofT
.let carProps: keyof Car; // the union of 'manufacturer' | 'model' | 'year'
-
T[K]
is the indexed access operator. Here the type syntax reflects the expression syntax:Car['model']
has the typeCar['model']
which here isstring
. However just like index type queries, you can useT[K]
in a generic context, which is where its real power comes to life. You have to make sure that the type variableK
doesextends
the typekeyof T
.
keyof T
Index type query operator allows you to create a string literal type union of the known public property names of T
:
interface Car {
model: string;
manufacturer: string;
year: number;
}
let carProp0: 'model' | 'manufacturer' | 'year' = 'model'
// equivalently
let carProp1: keyof Car = 'manufacturer';
// equivalently
type CarProps = keyof Car;
let carProp2: CarProps = 'year';
function f(a: typeof carProp1): typeof carProp2 {
return a;
}
function g(a: keyof Car): keyof Car {
return a;
}
function h(a: CarProps): CarProps {
return a;
}
let carProp3: [keyof Car, CarProps, typeof carProp0] = [f('model'), g('model'), h('model')]; // [ts] Error - Type '"model" | "manufacturer" | "year"' is not assignable to type '"manufacturer"'
The problem above comes from the fact that even though keyof Car
, CarProps
and typeof carProp0
produce the same type : 'manufacturer' | 'model' | 'year'
, when getting the type from a variable with typeof carProps0
only in the context of a tuple type definition, here: [typeof Car, CarProps, typeof carProp0]
, TypeScript limits the type in the 3rd position to the value of carProp0
which is only 'model'
.
Indexed Access Operator: T[K]
Indexed access operator the type syntax T[K]
reflects the type of the expression syntax car['model']
, which in our following example is string
.
function getProp<T, K extends keyof T>(o: T, propName: K): T[K] {
return o[propName];
}
let car: Car = new Car();
let prop0 = getProp(car, 'year'); // [ts] ok prop is of type 'number'
let prop1 = getProp(car, 'manufacturer'); // [ts] ok prop is of type 'string'
So T[K]
type syntax is refering to: the type of the property K
belonging to T
. And TypeScript is capable of telling what the actual return type will be depending on the paramters provided to getProp
.
Below, is an example of an array of such object properties. The array is of type: T[K][]
:
function getProps<T, K extends keyof T>(o: T, propNames: K[]): T[K][] {
return propNames.map(prop => o[prop];
}
let car: Car = new Car();
let props = getProps(car, ['year', 'manufacturer']); // [ts] ok props is of type 'number' | 'string'
You see above, that the array props
(containing the selected props of object car
) is of type array of type union of each of the selected props type, here: ('number' | 'string')[]
.
But look at this modification:
// ... getProps method as before
let car: Car = new Car();
let selection = ['year', 'manufacturer'];
let props = getProps(car, selection); // [ts] Error Argument of type 'string[]' is not assignable to parameter of type '("year" | "model" | "manufacturer")[]'
Above, the selection
variable is of type string[]
therefor it is too generic to use it as second parameter which requires an array of only the union of the string literal types: 'year'
, 'model'
or 'manufacturer'
. We can solve this by using keyof
operator:
// ... as before
let selection: (keyof Car)[] = ['year', 'manufacturer'];
let props = getProps(car, selection); // [ts] ok - props is of type ('string' | 'number')[]
Index Signatures
In JavaScript, an object can be accessed with a string to hold a reference to any other JavaScript object:
const obj = {};
obj["myprop"] = "some value";
console.log(obj["myprop"]); // "Some other object"
We store the object "some value"
using the string "myprop"
.
But if you pass any other object than a string as accessor, JavaScript's runtime will call .toString
on the accessor object.
const obj = {};
const notAString = { hey: "you", };
obj[notAString] = "some value";
console.log(obj[notAString]); // [js] "Some other object"
// but
console.log(obj[{ a: "different object", }]); // [js] "Some other object"
What happened above, is that JavaScript is calling .toString
on an instance of Object
, which returns the string "[object Objec]"
(if not explicitely overriden). So when using any instance of Object
as property accessor, JavaScript will access the key "[object Object]"
which is a valid property since it is a string.
We can circumvent this problem by overriding the default Object.toString
method inherited by any Object
instance.
const obj = {};
const accessorObj = {
a: "my stuff",
toString() {
return a;
}
};
obj[accessorObj] = "some value";
obj[{}] = "another value"
console.log(obj[accessorObj]); // [js] "some value"
console.log(obj[accessorObj.toString()]); // [js] "some value"
console.log(obj[{}]); // [js] "another value"
console.log(obj["object Object"]); // [js] "another value"
We have been able to change where "some value"
is stored by overriding the default Object.toString
method in accessorObj
. And when using another object {}
we sotre it under "[object Object]"
.
Note: Arrays are a bit different. For number
, indexing JavaScript VM's will try to optimize, so number
should be considered a valid object accessor in its own right (distinct from string
).
let foo = ["World"];
console.log(foo[0]); // [js] "World"
IMPORTANT: If we are not carefull, it is easy to inadvertently store a value under the wrong property when relying on JavaScript's default feature of calling .toString
under the hood for us. That is why TypeScript forces us to explicitely call .toString
on accessor objects that are not of type strings
or number
(Symbol
too).
In TypeScript we need to do the following:
const obj: any = {};
const accessorObj = {
a: "my stuff",
toString(): string {
return a;
}
};
obj[accessorObj] = "some value"; // [ts] Error - the index signature must be string
// FIX
obj[accessorObj.toString()] = "some value"; // [ts] Ok
console.log(obj[accessorObj.toString()]); // [ts] "some value"
You are not forced to explicitely call .toString
for string
and number
types, because out of the box, number
has a nice toString
implementation that returns the number in string
representation.
Declaring index signatures
TypeScript will allow us to set an index (when an Index Signature has not been defined priorly) only if we explicitely set the object type to any
:
let stringIndexObject = {};
stringIndexObject["hello"] = "you"; // [ts] Error = Element implicitly has an 'any' type because expression of type '"hello"' can't be used to index type '{}'. Property 'hello' does not exist on type '{}'.
// FIX
let anyObject: any = {};
anyObject["hello"] = "you"; // [ts] Ok
anyObject[1] = "you"; // [ts] Ok
Using any
is against TypeScripts philosophy, so a better fix would be to be more explicit using index signatures:
let numberIndexObject: { [index: number]: string, };
let numberIndexObject = {};
numberIndexObject[1] = "";
let stringIndexObject: { [index: string]: SomeOtherType, } = {};
stringIndexObject[(1).toString()] = "";
stringIndexObject[1] = ""; // [ts] Ok even if number for string index, relying on implicit javascript accurate runtime number conversion to string
Notice how TypsScript will prevent us from using any other index signature than the one defined in the object type (which are limited to string
and object
):
numberIndexObject["1"] = ""; // [ts] Error - Element implicitly has an 'any' type because index expression is not of type 'number'
numberIndexObject[(1).toString()] = ""; // [ts] Error - Element implicitly has an 'any' type because index expression is not of type 'number'
Note: You can change how you name the index signature, for TypeScript it does not affect anything. Change it to give some context, for example, if the object keys are user names, then we can define the object index signature as:
let users: { [username: string]: SomeUserData, };
IMPORTANT: as soon as you have a string
index signature (like [index: string]: someType
) all explicit members must also conform to that signature and return a someType
. This is to provide safety so that any string access returns the same result (Wtf??? same type?).
interface Foo {
[key: string]: number;
x: number;
y: string; // [ts] Error - Property 'y' of type 'string' is not assignable to string index type 'number'
}
// but with number it is ok
interface Bar {
[key: number]: string;
1: string;
x: string; // [ts] ok
y: number; // [ts] ok
}
Using limited set of string literals
An index signature can require that index strings be mebers of a union of literal strings by using Mapped Types:
type Index = 'a' | 'b' | 'c';
type FromIndex = { [k in Index]?: number };
const good: FromIndex = { b: 1, c: 2, };
const bad: FromIndex = { x: 1, b: 1, }; // [ts] Error - Object literal may only specify known properties, and 'x' does not exist in type 'FromIndex'
Specificatino of the vocabulary can be deffered to generically:
type FromSomeIndex<K extends string> = {[key in K]: number};
Having both string
and number
indexes
It is a very rare use case but TypeScript suppoorts it not the less
interface ArrStyr {
[key: string]: string | number; // Must accomodate all members
[index: number]: string; // Can be subset of string
length: number; // just an example number
}
Design Pattern: Nested index signature
Instead of using this:
interface NestedCss {
color?: string;
[selector: string]: string | NestedCSS | undefined;
}
const example: NestedCSS = {
color: "red",
".subclass": {
color: "blue"
},
}
// a typo in `color` is not catched
const failSilently: NestedCSS = {
colour: 'red', // [ts] ok even though there is a typo
}
To avoid these kind of situations, it is best to spearate the nesting into its own property:
interface NestedCSS {
color?: string;
nesting?: {
[selector: string]: NestedCSS;
}
}
const failsOnTypo: NestedCSS = {
color: "red",
nesting: {
'.some-sub-class': {
colour: "red", // [ts] Error - unknown property 'colour'
}
},
}
Excluding certain properties from the index signature
Sometimes you need to combine properties into the index signature. This is not advised, and you should use the Nested Index Signature Pattern mentionned above.
type FieldState = {
value: string;
}
type FormState = {
[fieldName: string]: FieldState;
isValid: boolean; // [ts] Error - Property 'isValid' of type 'boolean' is not assignable to string index type 'FieldState'
}
This is the error that we got previously, where once you define an index signature to have an index of type string
then all members must conform to that index return type (here isValid
should be FieldState
and not boolean
).
To work around this limitation (even though it is not advised) you can use an intersection type:
type FieldState = {
value: string;
}
type FormState = { isValid: boolean } & { [fieldName: string]: FieldState }
IMPORTANT: even though you can declare it to model existing JavaScript, TypeScript will not allow you to create an object of type FormState
. You will need to have imported the object from some library using declare:
import {foo} from 'foolibrary';
declare const foo: FormState;
const isValid: boolean = foo.isValid; // [ts] ok
const bar: FormState = {
isValid: false, // [ts] Error - Type '{ isValid: false; }' is not assignable to type 'FormState'. Type '{ isValid: false; }' is not assignable to type '{ [fieldName: string]: FieldState; }'. Property 'isValid' is incompatible with index signature.
}
Index Types and Index Signtures
keyof
and T[K]
interact with index signatures. An index signature parameter type must be 'string' or 'number'.
Limit explicit use of undefined
TypeScript gives you the opportunity to document your structures separately from values so instead of writing stuff like:
function foo() {
// if something
return {a: 1, b: 2, };
// else
return {a: 1, b: undefined, };
}
you should use type annotations:
function foo(): {a: number, b?: number, } {
// if something
return {a: 1, b: 2, };
// else
return {a: 1, };
}
Don't use undefined
as a means of denoting validity
An awful function like this:
function toInt(str: string) {
return str
? parseInt(str)
: undefined
}
can be much better written like this:
function toInt(): { valid: boolean, int?: number, } {
const int = parseInt(str);
if (isNaN(int)) {
return { valid: false, };
} else {
retunr { valid: true, int}
}
}
Object Oriented vs Functional Programming with TypeScript
To do object oriented, we perform inheritance by creating a base class from which others will inherit and then extend.
class Human {
constructor(public name) {} // this will add name as property
sayHi() {
return `Hello, ${this.name}`;
}
}
const patrick = new Human('Patrick Mullot');
class SuperHuman extends Human {
constructor(name) { // this without private, to not add it to SuperHuman
//but to override Human constructor
super(name);
this.heroName = `${name} the Hero!`;
}
superpower() {
return `${this.heroName} dabooom!!`;
}
}
const steph = new SuperHuman('Steph Curry');
console.log(steph.sayHi()); // Hello, Steph Curry
As an alternative to inheritance simple inheritance, is using composition through the mixin pattern. You decouple your properties into different objects that functions return, and then you compose them back using a final function. This is actually a way of doing multiple inheritance, which is what you wish exists, when you realize that your classification design is poor. Crockford does not advocate for this, but let's see it anyway.
const hasName = function (name) {
return { name, };
};
const canSayHi = function (name) {
return {
sayHi: function () {
return `Hello, ${name}`;
},
};
};
const Person = function (name) {
return {
...hasName(name),
...canSayHi(name),
};
};
const sally = Person('Sally');
sally.sayHi(); // Hello, Sally
In it's current form, we loose all of the class
based ergonomics. But TypeScript gives us a way to use mixins in a type based format.
This is what we use to create the mixin from the classes we define after.
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
class CanSayHi {
name;
sayHi() {
return `Hello, ${this.name}`;
}
}
class HasSuperPower {
heroName;
superpower() {
return `${this.heroName} dabooom!!`
}
}
// instead of extending, we are going to implement
class SuperHero implements CanSayHi, HasSuperPower {
heroName;
constructor(public name) {
this.heroName = `${name} the Hero!`;
}
sayHi: () => string;
superpower: () => string;
}
// now the magic:
applyMixins(SuperHero, [CanSayHi, HasSuperPower]);
const ts = new SuperHero('TypeScript');
When you are implementing something, you are only concerned with its interface and not its underlying code. When implementing stuff we have the additional boilerplate code of telling TypeScript what the return values of the methods are (above sayHi
and superpower
)
Now let's answer the fundamental question: "is a hot dog a sandwitch"?
- If you use inheritance, then you will have to inherit some base
Sandwich
class, which means it is aSandwich
- If you use composition, then you can just pull a
Sausage
and aBun
which means it is not aSandwich
Functional programming Deepu
First-class and higher-order functions
Is conveying the idea that functions a re a first class citizen, meaning that you can pass a function as an argument to another function, or return a function from another etc. Typescript supports this and makes concepts like: closures, currying and higher-order-functions easy to write.
Higher Order Function
A function is considered of higher-order-function if any of the following is true:
- it takes one or more functions as parameters
- it returns another function as a result
type mapFn = (it: string) => number;
// the HOF takes an array and a function as arguments
function mapForEach(arr: string[], fn: mapFn): number {
const newArray: number[] = [];
arr.forEach(if => {
// we are executing the method passed
newArray.push(fn(it));
});
return newArray;
}
const list = ["Orange", "Apple", "Peach"];
const out = mapForEach(list, (it: string): number => it.length); // [6, 5, 5,]
the eample above is very convoluted for a very simple case, but you get the idea
const list = ["Orange", "Apple", "Peach"];
const out = list.map(it => it.length; // [6, 5, 5]
Closures and Currying are also possible
// this is a higher order function that returns a closure
function add(x: number): (y: number) => number {
// a function is returned here as closure
// variable x is obtained from the outer scope
return (y: number): number => x + y;
}
const add10 = add(10)
const add20 = add(20);