Read [tdd vs. statically typed][tdd-v-static] to get a refresher on why statically typed languages are an advantage for large projects.
Installation
You can check whether you have the typescript compiler installed type tsc
in the command line.
To install typescript you can use npm
. Use the command:
npm i -g typescript
This will install the tsc
compiler globally.
Compiling a .ts file into javascript
tsc myscript.ts
This will output a .js
file. This is the file that can run on the browser and that you would import in an html file through the:
<script src="./myscript.js"></script>
You can also call the compiler passing [options][tsc-compiler-options]:
tsc myscript.ts --noImplicitAny --target "ES5" --module
Config strict checking
To enforce very strict checks there are 3 directives:
-
noImplicitAny
: sometimes typescript isn't able to figure out the type of a variable, in those cases it will use typeany
implicitly in its place. So this means you will not get the type safety there, and tooling support. If you want to get an error in these occurrences, then use this option. -
strictNullChecks
: (dependencies may need to be updated) by default typescript assumes thatnull
andundefined
are in the domain of every type. WhenstrictNullChecks
is enabled,null
andundefined
get their own types callednull
andundefined
respectively. Whenever anything is possibly null, you can use the|
type union operator to tell typescript thatnull
is also allowed there. For example,number | null
tells typescript that the variable can be anumber
ornull
. Note : if you ever have a value that typescript thinks isnull
but you know better, you can tell typescript to shut up with:declare const foo: string[] | null; foo.length; // error - foo is possibly null (so no .length method on it) foo!.length; // okay - 'foo!' is always going to be type string[] (even though its declaration allows null)
-
noImplicitThis
: No implicitany
forthis
. When you use thethis
keyword outside of classes, thethis
can beany
type. Even when you attach it to the prototype, of some other object, typescript assumes this to be any type. And you can easily misspell a property ofthis
within the function, and never notice. SonoImplicitThis
complains whenever you are not enforcing athis
type:// ... Point class somewhere else with getDistance method // reopen the interface interface Point { distanceFromOrigin(point: Point): number; } Point.prototype.distanceFromOrigin = function(this: Point, point: Point) { return this.getDistance(point); // getDistance belongs to class Point defined somewhere else }
Mosh hamedani's type script introduction
Classes, using typescript. Private members, short hand constructors, auto assignment getters and setters
class Point {
constructor(private _x?: number, public y?: number) {
// no need for this.x = x etc. typescript generates that for us.
}
draw() {
console.log(this._x, this.y);
}
// give read access
get x() {
return this._x;
}
// give write access but with conditions
set x(value) {
if (value < 0)
throw new Error('Negative values are not allowed');
this._x = value;
}
}
let point = new Point(1, 2); // no need to type "let point: Point = new Point(1, 2);
point.draw() // we still benefit from hinting of the class Point
A convention to be able to define getter and/or setter methods on private members, is to prefix the private member with an underscore, and then use the name without underscore for the setter and getter method names. These methods are called respectively on obj.x = 'something'
and let x = obj.x
and there exists no naming collision.
Typescript has the enum
type which lets you define a set of constants related to each other.
Traversy
What does typescript offer:
- Static type checking
- Class based objects
- Modularity
- ES6 features
- Syntax closer to java and other high level languages
Typescript types
- String
- Number
- Boolean
- Array
- Any
- Void
- Null
- Undefined
- Unknown
- Tuple
- Enum
- Generics
let numArra: number[];
let numberArray: Array<number>;
let strArray: string[];
let stringArray: Array<string>;
let boolArray: boolean[];
let boolArray: Array<boolean>;
let tuple: [string, number];
tuple = ['asdf', 1] // good
let myVoid: void = null; // works fine
let myVoid: void = undefined; // works fine too,
let myVoid: void = 1; // compilation error
let myUndef: undefined = null; // works fine too
let myNull: null = undefined; // works fine too
let x: number = null; // [ts] Error Type 'null' is not assignable to type 'number'
let y: number = undefined; // [ts] Error: Type 'undefined' is not assignable to type 'number'
let z: number;
console.log(z); // [ts] Error: Variable 'z' is used before being assigned
Functions
--noImplicitThis
will warn you when this is not explicitly bound to some object and therefore tell you that this
is of type any
.
The this
keyword has a special meaning in JS, and more so in TypeScript.
class Hello {
hi() {
return function() {
return this; // Error: 'this' implicitly has type 'any' because it does not have a type annotation.
}
}
ho() {
return () => {
return this; // Works.
}
}
}
const h = new Hello(); // [ts] h: Hello ;; [js] instance of Hello
const a = h.hi(); // [ts] a: () => any
const aCalled = a(); // [ts] aCalled: any ;; [js] global object
const b = h.ho(); // [ts] b: () => Hello
const bCalled = b(); // [ts] bCalled: Hello ;; [js] instance of Hello, same as in h
A quick explanation of what is happening above: in the hi()
method, which is returning a function which in turn returns this
. Remember, in javascript, the this
keyword will refer to the pre-dot object to which the function is attached to at call time. And in case of a standalone call, to the global object. So in the hi()
case, the this
within the inner returned function has no ts type.
On the other hand, the arrow function retains the value of this
from the parent scope where it was defined. Called the lexical scope or lexical this. Which means that this
does retain the Hello object, and ts knows it too.
this
parameter
Contrary to javascript, typescript allows you to set this
as the first parameter to any function. This will tell typescript what this
is supposed to refer to.
Forbidding standalone usage of a function this: void
You can prevent usage of this
in a function by setting the function's first parameter to be this: void
let h = function(this: void) {
return this.hello; // Error : Property 'hello' does not exist on type 'void'.
}
Telling the compiler what this
refers to
class Hello {
hi(this: Hello) {
return function() {
this.ho(); // [ts] Error 'this' implicitly has type 'any' because it does not have a type annotation
this.notHere(); // [ts] Error 'this' implicitly has type 'any' because it does not have a type annotation
return this; // [ts] Error 'this' implicitly has type 'any' because it does not have a type annotation
}
}
ho(this: Hello) {
return () => {
this.ho(); // [ts] Works
this.notHere(); // [ts] Error: Property 'bee' does not exist on type 'Hello2'
return this; // [ts] Works: 'this' has type Hello.
}
}
}
Above, the problem that ts is complaining about in function hi(this: Hello)
is the same as when no type was specified because the returned function will loose the this
scope anyway. On the other hand, ho(this: Hello)
using the arrow function is enough to let ts know the return type, but note that without the type annotation, ts knew the return type aswell, so what is the benefit?.
this
parameter in callback
You can also run into errors with this
in callbacks, when you pass functions to a library that will later call them. Because the library that calls your callback will call it like a normal function, this
will be undefined
. With some work you can use this
parameter to prevent errors with callbacks too.
-
First the library author needs to annotate the callback type with
this
:interface UIElement { addClickListener(onClick: (this: void, e: Event) => void): void; }
- The
this: void
means thataddClickListener
expectsonClick
to be a function that does not requirethis
type.
- The
-
Second, annotate your calling code with
this
:class Handler { info: string; onClickBad(this: Handler, event: Event): void { // Wrong! Although it would compile, it will still crash at runtime this.info = event.message; } } let h = new Handler(); // ... somewhere else in the library // uiElement implements UIElement interface above uiElement.addClickListener(h.onClickBad); // Error!
- With the
this: Handler
we make it explicit thatonClickBad
is expectingthis
to be of typeHandler
, so that when we passonClickBad
toaddClickListener
TypeScript will detect the incompatibility with theinterface UIElement
which requiresthis: void
.
- With the
To Fix the error we need to change the code a bit:
class Handler {
info: string;
onClickGood(this: void, event: Event): void {
// Works, but we cannot use 'this' anymore, because it is expected to be void
console.log('Good, but no "this" usage allowed anymore in here, but runs ok' + event.message);
}
}
let h = new Handler();
// ... somewhere else in the library
// uiElement implements UIElement interface above
uiElement.addClickListener(h.onClickGood); // Works!
The problem now is that with the solution above, if we want to use the this
keyword then we'll need to use an arrow function as a property (which does not allow overriding from subclasses, and which is copied over every object, whereas methods are stored in the prototype). Like so:
class Handler {
info: string;
onClickGood: (event: Event) => {
this.info = event.message;
}
}
The above works because arrow functions capture the other this
, so you can always pass them to something that expects this: void
.
Function types
Functions belong to the function type
which typescript figures out whenever you are declaring a function. Typescript gives you the ability to specify how a specific function is typed:
- its argument types
- its return type.
Therefore the function type is a superset of all the function types with specific argument counts, types and return types.
function sum(a: number, b?: number): number {
if (b == undefined) {
return a;
}
return a + b;
}
function myvoid(name: string): void {
console.log('hello ' + name);
}
Named function vs. Anonymous Function
function namedFunc(a, b) { a + b; };
let anonymousFunc = function(x, y) { return x + y; };
Typing Functions
We can explicitely specify the function return type, but in many cases TypeScript is able to figure out the return type by itself from the type of the parameters.
function add(x: number, y: number): number {
return x + y;
}
// typescript knows that the return type is a number
function addWithImplicitReturnType(x: nubmer, y: number) {
return x + y;
}
One thing is to type a function, as above: giving the function definition a few constraints on the argument counts, types and return types. And another, is to tell typescript the type of a certain variable. Two function definitions may share a same type.
function a(hello: string, name?: string) {
return hello + name;
}
function b(hello: string, name = "Bob") {
return hello + name;
}
// Both functions above share the type
let ab: (p: string, q?: string) => string;
The default value of name
is not held in the type.
Note: When using default-initialized parameters, they need not be at the end of the parameter list. The only requirement is that undefined
is passed in their location when the user expects to use the default.
Note: The type of the default parameter is inferred from the default value.
function buildName(name = "Bernard", lastName: string) {
return name + " " + lastName;
}
let bn: string = buildName(undefined, "Montiel"); // ok "Bernard Montiel"
let bn: string = buildName("Montiel"); // Error: too few parameters ( "Montiel" is assigned to 'name' and 'lastName' is empty when it should be )
Below what we are doing is storing the function on the right side ( which has types for parameters and for the return value, and is of a certain type itself ) into the variable myAdd
.
let myAdd : (x: number, y: number) => number = function(x: number, y: number): number { return x + y; };
Below is the right hand side of the expression above. This is the part where we actually define the function and typescript infers its type.
function(x: number, y: number): number { return x + y; };
and what Typescript remembers of the above's function type is:
(x: number, y: number) => number;
Now if we want to store this function into some variable, like above in myAdd
, we need to tell typescript what type is going to be store in the variable. We could do it in two ways:
-
Let typescript figure out the type of
myAdd
directly from the assignment.let myAdd = function(x: number, y: number): number { return x + y; };
-
Tell typescript which type is the variable
myAdd
going to hold, and then store the function matching that type in it. This second way is the same as the initial example, except it is performed in two steps.// tell Ts what type is going to be held let myAdd: (x: number, y: number) => number; // assign a value matching that type myAdd = function(x: number, y: number): number { return x + y; }
Note: the part after the fat arrow =>
indicates the return type of a function type. And it is mandatory. In case a function returns undefined or never, you would use void
or never
respectively:
let returnsUndefined: (message: string, value: number) => void;
let neverReturns: (foreverMessage: string) => never;
Rest parameters
Required and default parameters, have one thing in common: they all talk about a single parameter at time.
Sometimes you may want to:
- talk about many parameters at a time
- avoid specifying the exact count of parameters in advance
Note: javascript allows you to use the arguments
variable within a function to access the parameter list.
In TypeScript you can use ...rest
paramters like so:
function buildName(first: string, ...restOfParams: string[]): string {
return first + restOfParams.join(" ");
}
let employeeName: (a: string, ...b: string[]) => string = buildName("John", "Mc", "Kenzy", "The", "Great");
Colon :
vs. Fat Arrow =>
You may wonder why sometimes the return type of a function is specified using a colon :
and other times with using a fat arrow =>
. The reason is that using the :
may be ambiguous depending on the context where it is used. For example, given a function that accepts a callback, using :
to represent the return type is ambiguous and results in a syntax error, we can overcome it with the fat arrow notation or by wrapping the the callback parameter type in {}
:
// ambiguous parse, syntax error
function sendString(callback: (value: string): void);
// valid, using fat arrow
function sendString(callback: (value: string) => void);
// same thing, using curly brackets
// (harder to write, harder to parse visually)
function sendString(callback: { (value: string): void; });
In classes it is possible to use the fat arrow notation for properties of a class that hold a function, but not for methods.
class Foo {
// This actually a property in typescript's eyes, not a method
someMethod: (v: string) => boolean;
}
In this case TypeScript makes the distinction between methods of a class and properties of a class. Functions defined as properties like above, don't need a body, but cannot be overridden or defined as methods in subclasses. In other words you couldn't do this even though the signatures are compatible:
// Error cannot do this
class Bar extends Foo {
someMethod(v: string): boolean {
return v.length > 0;
}
}
In conclusion, due to how syntax works in TypeScript, it is always possible to use the :
syntax for a function type (use {}
if needed), but it is not always possible to use the =>
syntax.
Function Overloads
Sometimes a function will return different types depending on way it is called. From a mathematic perspective they are two separate functions with separate input sets and output mappings. So they could be separated in two. In this sense, TypeScript allows you to use overloads, which define the different function types that the function can have and thus enable type checking.
let suits = ["hearts", "clubs", "diamonds", "spades"];
function pickCard(x: any): any {
if (typeof x === "object") {
let pickedCard = Math.floor(Math.rand() * x.length);
return x[pickedCard];
} else if (typeof x === "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13, };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
The above function's type can be narrowed down to two. Instead of the very generic any, we can use function overloads like so:
// overload for when it is supplied with a deck of cards
function pickCard(x: { suit: string; card: number; }[]): number;
// overload for when it is supplied with a deck of cards
function pickCard(x: number): { suit: string; card: number; };
// compatible type function definition (the type should be a superset of the overloads) BUT is not an overload in itself!
function pickCard(x: any): any {
if (typeof x === "object") {
let pickedCard = Math.floor(Math.rand() * x.length);
return x[pickedCard];
} else if (typeof x === "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13, };
}
}
With this change, the overloads now give us type checked calls to the pickCard
function.
IMPORTANT: It looks at the overload list and, proceeding with the first overload, attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.
Note: Note that the function pickCard(x): any piece is not part of the overload list, so it only has two overloads: one that takes an object and one that takes a number. Calling pickCard with any other parameter types would cause an error.
Interfaces
One of TypeScript's core principles is that type-checking focuses on the shape that values have also known as "structural subtyping". In typescript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts outside of your project.
IMPORTANT: contrary to languages like PHP, C# or Java, you don't need to explicitly implement the interface in order for the compiler to consider their signature compatible. Only the shape matters in typescript.
function showTodo(todo: {title: string, text: string, }): void {
console.log('This is my todo: Title ' + toto.title + ', text ' + todo.text);
}
let myTodo = {title: 'asdf', text: ' hello' };
showToto(myTodo);
//A cleaner way to do this would be to create an interface
inteface Todo {
title: string,
text: string,
}
function showTodo(todo: Todo): void {
console.log('This is my todo: Title ' + toto.title + ', text ' + todo.text);
}
// an even better way would be to create a class, because of the unity principle. If it has lots of connection with some other part, then it should be together
Classes
When comparing types that have private
and protected
members, we treat these types differently. For two types to be considered compatible, if one of them has a private
member, then the other must have a private
member that originated in the same declaration. The same applies to protected
members.
interface UserInterface {
name: string;
email: string;
age: number;
payInvoice(): void;
}
class User implements {
name: string;
protected email: string; // only inheritants can use this
private age: number; // adding an access modifier
constructor(name: string, email: string, age: number) {
this.name = name;
this.email = email;
this.age = number;
}
payInvoice(): void {
console.log('Pay invoice');
}
}
let user = new User('John', 'my@mail.com', 21)
Types
You have to think of types as sets. Sets of possible values.
Basic Types
Boolean
The most basic data type is the simple true
/false
boolean value:
let isDone: boolean = false;
Number
As in javascript all numbers in TypeScript are floating point values. These floating point numbers get the type number
.
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
String
let n: number = 21;
let multilineString: string = `Hello.
Some template string with number ${n + 1}
`;
Array
TypeScript like javascript allows you to work with arrays of values. Array types can be defined in two ways.
-
number[]
: type of the elements followed by[]
let list: number[] = [1, 2, 3]; let otherList: string[] = ["", 1]; // error
-
Array<number>
:Array
followed by the type name between<>
let list: Array<number> = [1, 2, 3]; let otherList: Array<string> = ["", 1]; // error
Tuple
Tuples allow you to express an array with a fixed amount of elements of some specific types:
let tup: [string, number, boolean];
tup = ["Hello", 1, true];
tup = ["Hello", 1, 0]; // Error
When accessing an element with a known index, the correct type is inferred:
console.log(tup[0].substring(2)) // works
console.log(tup[1].substring(2)) // Error 'number' does not have 'substring'
When setting an element with an invalid index, an error is thrown :
tup[3] = ""; // Error, property 3 does not exist on type [string, number, boolean]
Enum
A helpful addition to the standard set of data types in JavaScript is the enum
. As in C#, enum
are a way of giving more frienly names to a set of numeric values.
enum Color = {Red, Green, Blue};
let c: Color = Color.Green;
By default enums beggin numbering their members at 0
. But that can be changed by either manually setting the first value, or setting them all manually. Then you can even get the name of the index in the enum as a string
enum Color = {Red=3, Green, Blue};
let c: Color = Color.Green; // 4
enum Color = {Red=10, Green=15, Blue=20};
let c: Color = Color.Green; // 4
let colorName: string = Color[15];
console.log(colorName); // 'Green'
Any
We may need to describe the type of variables whose value we do not know at the time of writing the code. These values may come from dynamic content (e.g. from the user or 3rd party libraries). In this case, we want to opt out of type checking, and let the values pass through compile time checks. To do so, we label the type of those variables as any
.
let notSure: any = 4;
notSure = "Why not a string now!"; // ok
notSure = false; // ok
The any
type can allow you to work with existing javascript. Allowing you to gradually opt in and opt out of type checking during compilation.
Difference between Any and Object
You might expect Object
to be as obscure as any
, but the difference is that whereas TypeScript will not complain on arbirary property/method calls to an any
variable, it will definitely not allow you to call arbitrary methods on variables of type Object
. That's mainly because variables of type Object
can host any type of object as long as typeof v === 'object'
, but once one is placed into the variable, only methods of that specific object will be allowed to be called by TypeScript.
Check difference between Object and object in TypeScript.
let notSure: any = 4;
notSure.ifItExists(); // Ok - ifItExists might be available at runtime
let prettySure: Object = 4;
notSure.ifItExists(); // Error: Property 'ifItExists' doesn't exist on type 'object'
You can also have type any
arrays, which allow the array to contain anything
let a: any[] = [1, "b", new Hey()];
a.push("Something Else");
Void
void
is somewhat like the opposite of any
the absence of having any type at all. You may commonly see this as the return type of functions that do not return a value. Or which return undefined
in JavaScript. So basically void
is like the type undefined
but for function returns. Actually you can also use it for variables, and it will let you store null
(only if --strictNullChecks
is not specified) or undefined
.
function f(): void {
console.log("No return value");
}
let unusable: void = undefined; // what's the point? None
unusable = null; // only if --stricNullChecks is not enabled, but still useless
Null & Undefined
null
and undefined
are actually their own types. Much like void
they are not extremely usefull on their own.
let u: undefined = undefined; // only acceptable value for u
let n: null = null; // only acceptable value for n
By default null
and undefined
are included in all other types. That means you can assign null
and undefined
to something that is of type number
for example.
Note: if --strictNullChecks
is enabled, then null
and undefined
are only assignable to variables of type any
(well undefined
can also be assigned to void
). In that case you need to use type unions in order to allow having variables that can hold string
and null
and undefined
with: string | null | undefined
.
Object
object
(with lowercase o
) is a type that represents the non primitive types i.e. anything that is not in:
number
string
boolean
symbol
null
undefined
With object
type, API's like Object.create
can be better represented. For example:
declare function create(o: object | null): void;
create({prop: 0}); // Ok
create(null); // Ok because we added the union
create("hello"); // Error
create(54); // Error
create(undefined); // Error
create(false); // Error
Object vs object
object
with lowercase o
is anything that is non primitive type. Whereas Object
are the functionalities of every object in javascript, like .hasOwnProperty(s: string): boolean
or .toString(): string
etc.
Here is the definition of type Object
:
interface Object {
// ...
/** Returns a string representation of an object. */
toString(): string;
/** Returns a date converted to a string using the current locale. */
toLocaleString(): string;
/** Returns the primitive value of the specified object. */
valueOf(): Object;
/**
* Determines whether an object has a property with the specified name.
* @param v A property name.
*/
hasOwnProperty(v: string): boolean;
/**
* Determines whether an object exists in another object's prototype chain.
* @param v Another object whose prototype chain is to be checked.
*/
isPrototypeOf(v: Object): boolean;
/**
* Determines whether a specified property is enumerable.
* @param v A property name.
*/
propertyIsEnumerable(v: string): boolean;
}
Type assertions
Sometimes you will end up in a situation where you will know better about a value than TypeScript does. This usually will happen when you know the type of some entity could be narrower than its current type.
Type assertions are a way to tell the complier "trust me, I know what I'm doing!". A type assertion is like a type cast in other languages, but performs no special checking or restructuring of the data. It has no runtime impact and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.
Type assertions have two forms:
<>
angle bracket (not allowed with JSX)as
syntax (works with JSX)
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
strLength = (someValue as string).length;
Both notations are equivalent. But when using TypeScript with JSX, only the as
notation is allowed.
Tuple destructuring
let tuple: [number, string, number] = [1, "one", 2];
let [a, b, c] = tuple; // a: number, b: string, c: number
cannot attempt to destructure more than what the tuple contains
let tuple: [number, string, number] = [1, "one", 2];
let [a, b, c, d] = tuple; // Error: no element at index 3, whereas in Javascript this would just make e === undefined
except if you destructure it into a rest, which produces an empty tuple
let tuple: [number, string, number] = [1, "one", 2];
let [a, b, c, ...d] = tuple; // a: number, b: string, c: number, d: [] empty tuple
You can also destructure a tuple into a smaller tuple with the rest operator
let tuple: [number, string, number] = [1, "one", 2];
let [a, ...b] = tuple; // a: number, b: [string, number]
let [ , , c] = tuple; // c: number
Object destructuring
As with array destructuring. An interesting feature is destructuring without declaration (note the user of parenthesis, Javascript parses {
as the start of a block):
let a: string, b: number;
({a, b} = {a: "hi", b: 12});
Object property renaming
You can destructure object properties into variables with different names than the properties themselves. But note how using the colon does not mean the type but rather the name of the variable
let o = {a: "hi", b: 12};
let {a: renamedToThis, b: renamedToThat} = o;
console.log(renameToThis); // "hi"
You can still specify the type with
let o = {a: "hi", b: 12};
let {a, b}: { a: string, b: number } = o;
Default values
Default values let you specify the value in case a property is undefined:
function hello(wholeObject: { a: string, b?: number }): voic {
let { a, b = 100 } = wholeObject;
}
Function Declarations
Destructuring also works in function declarations.
type C = { a: string, b?: number };
function hello({ a, b }: C): void {
let { a, b = 100 } = wholeObject;
}
But specifying defaults is more common for parameters, and getting defaults right can be tricky. First of all you need to remember to put the pattern before the default value. (This is an example of type inference)
function f({ a = "", b = 0 }: {}): void {
//...
}
Then you need to remember to give a default for optional properties on the destructured property instead of the main initializer. Remember that C
was defined with b
optional.
function f({ a, b = 0 } = { a = "" }): void {
console.log(a, b);
}
f({}); // Error - 'a' needs to be present in the supplied argument object
f({b: 1}); // Error - 'a' needs to be present in the supplied argument object
f(); // Good - the default parameter { a = "" } will be used and default destructured 'b' value as well, logs: "", 0
f({a: "asdf"}); // Good - the default parameter { a = "" } will be overridden and default destructured 'b' value will be used, logs: "asdf", 0
f({a: "asdf", b: 1 }); // Good - the default parameter { a = "" } will be overridden and default destructured 'b' as well, logs: "asdf", 1
The above is using destructuring (on the left of the middle =
) for the function's single parameter, which destructured into { a, b = 0 }
where b
is optional. Then the right side of the middle =
is giving a default value to that single parameter with { a = "" }
. Since b
has a default value inside the destructuring, the resulting single parameter object will have a b
property = 0
if the function f
is not called with an object containing a property b
(in which case, that b
will override the default).
Spread
Arrays
let first = [1, 2]
let second = [4, 5]
let combined = [0, ...first, 3, ...second, 6]
Objects
let defaults = { food: "spicy", price: "$$", ambiance: "noisy", };
let search = { ...defaults, food: "rich", value: "Hey", }; // food overrides defaults: good
// { food: "rich", price: "$$", ambiance: "noisy", value: "Hey"}
let search2 = { food: "rich", ...defaults, }; // defaults override food, which is not what we want
// { food: "spicy", price: "$$", ambiance: "noisy", value: "Hey"}
Object spreads have limitations:
- It only spreads own enumerable properties: you loose methods
- TypeScript compiler does not allow spread of type parameters from generic functions. That feature is expected in future versions of the language
Interfaces bis
TypeScript is a "Duck Typing" meaning that if it "flies and quacks like a dock, then it is a duck". It's not the name of the interface that matters, it's the actual shape of the object that is being passed in the call that matters. It has to at least conform, but can also have more properties than the required ones.
function printLabel(labeledObject: { label: string }) {
console.log(labeledObject.label);
}
printLabel({ a: "asdf", label: "Some Label" }); // Ok : "Some Label"
printLabel({ a: "asdf" }); // Error
Now the same example using loger notation, with explicit interface declaration
interface Labeled {
label: string;
}
function printLabel(labeledObject: Labeled) {
console.log(labeledObject.label);
}
printLabel({ a: "asdf", label: "Some Label" }); // Ok : "Some Label"
printLabel({ a: "asdf" }); // Error
Optional properties
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string, area: number } {
let square = { color: "white", area: 10 };
if (config.color) {
square.color = config.color;
}
if (config.width) {
square.area = config.width * config.width;
}
return square;
}
let s = createSquare({ width: 100 }); // { color: "white", area: 10000 }
Readonly properties
Some properties should only be modifiable on creation, and then only readable. You can use interfaces for that with the keyword readonly
.
interface Point {
readonly x: number;
readonly y: number;
}
let p: Point = {x: 1, y: 1};
p.x = 5; // Error readonly !
By assigning an object literal to a variable with type Point
, we make it's properties unchangeable (readonly
).
Readonly Array
Typescript comes with the ReadonlyArray<T>
which is the same as Array<T>
with all mutating methods removed, making sure you do not change the Array after creation.
let a: number[] = [1, 2, 3]
let ro: ReadonlyArray<number> = a;
ro.push(4); // Error !
ro[0] = 4; // Error !
ro.length = 100; // Error !
a = ro; // Error ! cannot assign ReadonlyArray<number> to Array<number>
a = ro as number[]; // Ok type assertion (type casting) is allowed
Readonly vs. const
readonly
is to properties what const
is to variables.
Excess Property Checks
Optional properties in interfaces give you the possibility to not pass them. And what can happen is that if you try to pass a misspelled property as an object literal, you might inadvertently insert a bug.
interface SquareConfig {
color?: string;
width?: number;
}
function createSqaure(config: SquareConfig): { color: string, area: number } {
// ...
}
let s = createSquare({ colour: "Orange" }); // error: Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
TypeScript does excessive property checking when passing an object literal as the parameter. This is to avoid misspelled properties in the object literal. You can easily get around these checks with type assertions
let s = createSquare({ colour: "Orange" } as SquareConfig); // Works, but with a bug inserted !
Another approach is to allow additional properties within the interface itself, without mentioning their explicit names. The following allows having both color
and width
with their respective types, but additionally any other property of any type, this is using index signatures.
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
Another option is to assign the object literal to some variable. Since the variable is no longer an object literal, typescript will no longer perform excessive type checks so the compiler will not give you any error.
let config = { colour: "Orange" };
let s = createSquare(config); // works fine but with a bug inserted !
In the majority of cases, excess property errors are actually caused by bugs, so you should not try to bypass those checks. And rather revise your type definitions and add properties to your types if needed. However, for more complex object literals that have methods and hold state, you might need to keep these techniques in mind.
Function Types
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function(src: string, subStr: string): boolean {
let result = src.search(subStr);
return result > -1;
}
Note: type checks pass even if the parameter names do not match. Only types in parameter positions need to match.
Function parameters are checked one at a time, with the type in each corresponding position checked against each other. If you do not want to specify types at all, TypeScript's contextual type checking can infer types, since the function value is assigned directly to a variable of type SearchFunc
. Here also the return type of our function expression is implied by the values it returns (here false
and true
). Had the function expression returned or numbers or strings, the type checker would have warned us that the type did not match the return type described in SearchFunc
.
let mySearch: SearchFunc;
mySearch = function(src, substr) {
let result = src.search(substr);
return result > -1;
}
Indexable Types
Similarly to how we can use interfaces to describe function types, we can also describe types that we can "index into" like a[10]
, or ageMap["Daniel"]
. Indexable types have an index signature that describes the types that we can use to index into an object, along with the corresponding return types when indexing. Let's take an example;
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
Above we have a StringArray
interface that has an index signature. This index signature states that when StringArray
is indexed with a number
it will return a string
.
There are two types of supported index signatures: string and number. It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number
, Javascript will actually convert that to a string
before indexing into an object. That means that indexing with 100
(a number
) is the same as indexing with "100"
(a string
), so the two need to be consistent.
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog; // Error: getting an animal with numeric index might return you a totally different type of Animal (numeric index should return Animal, but since it is converted to string first, and strings return Dogs, the numeric may return Dog as well.)
}
While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that all properties match their return types. This is because a string index declares that obj.property
is also available as obj["property"]
. In the following example, name
's does not match the string
's index type, and the type checker gives an error.
interface NumberDictionary {
[index: string]: number;
length: number; // Ok length is a number, matches above
name: string; // Error: name is not a number, does not match the string index type `number`
}
interface NumberDictionary {
[index: string]: number | string;
length: number; // Ok length is a number, matches above
name: string; // Ok`both are tolerated now
}
We can also make index signatures readonly
in order to prevent assignments to their indices.
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let sa: string[] = ["Hey", "These", "Elements", "Will", "Never", "Change"]
sa[0] = "Change Now!"; // Error - index signature is readonly
Class Types
Implementing in interfaces
One of the most common uses of interfaces in languages like C# and Java, that of explicitly enforcing that a class meets a particular contract is also possible in Typescript.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) {}
}
You can also describe methods in an interface that are implemented in the class, as we do setTime
in the below example:
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date): void {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
interfaces describe the public side of the class, rather than both the public and private side of the class. This prohibits you from using the to make sure that a class uses specific types for the private side of the class.
Difference between Static and Instance side of classes
When working with classes and interfaces it is helpful to keep in mind that the class has two types: the type of the static side and the type of the instance side. You may notice that if you try to create an interface with a construct signature and try to create a class that implements this signature, you get an error.
interface ClockConstructor {
new (hour: number, minute: number);
}
// Error!
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) {}
}
This is because when a class
implements
an interface
only the instance part of the class
is checked against the interface
. Since the constructor
sits in the static
side of the class
it is not included in the check. Therefore the interface
instance requirement is not met by the class
in the example above.
Instead, you would need to work with the static side of the class directly. In the following example, we define two interfaces: ClockConstructor
for the constructor and ClockInterface
for the instance side of the class.
interface ClockConstructor {
new (hour: number, minute: number);
}
interface ClockInterface {
tick(): void;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) {}
tick() {
console.log('beep beep');
}
}
class AnalogClock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) {}
tick() {
console.log('tick tock');
}
}
let d = createClock(DigitalClock, 12, 0);
let a = createClock(AnalogClock, 7, 32);
Because createClock
first parameter is of type ClockConstructor
the argument passed to it, must implement it. We know from ES5 to ES6 that class
notation is syntactic sugar for simple the former function name notation, this is useful to understand that the name of the class, is actually a reference to the function containing the constructor in it's prototype (which is invoked with the new
keyword).
Another simple way is to use class expressions:
interface ClockConstructor {
new (hour: number, minute: number);
}
interface ClockInterface {
tick(): void;
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log('beep beep');
}
}
Here we do the same thing, we first create the class
Clock
on the right hand side of the =
enforcing the implementation of the ClockInterface
, then we store the whole expression into a constant Clock
on which we enforce the ClockConstructor
. By the same reason as above, what we store in const Clock
is actually the constructor function, which has a prototype with the tick
and constructor
etc.
Extending interfaces
Like classes, interfaces can extend each other:
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
You can compose interfaces from multiple others:
interface Shape {
color: string;
}
interface Bounceable {
bounce(x: number, y: number, force: number): void;
}
interface BouncableSquare extends Shape, Bounceable {
sideLength: number;
}
Hybrid types
As mentioned earlier, interfaces can describe rich types present in real world javascript. Because of JavaScript's dynamic and flexible nature, you may occasionally encounter an object that works as a combination of some of the types described above.
One such example is an object that acts both as a function and an object, with additional properties.
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = (function(start: number) {}) as Counter;
counter.interval = 100;
counter.reset = function() {};
return counter;
}
let counter = getCounter();
counter(10);
counter.reset();
counter.inverval = 10;
When interacting with 3rd party Javascript libraries you may need to use patterns like these to fully describe the shape of the type.
Interfaces Extending Classes
When an interface type extends a class type, it inherits its members but not their implementation. Interfaces even inherit the private
and protected
members of the base class. This means that when you create an interface that extends a class with private
or protected
members, that interface can only be implemented by that class or subclass of it.
This is useful when you have a large inheritance hierarchy, but want to specify that your code works with only subclasses that have certain properties. The subclasses don't need to be related, other than inheriting from the base class.
class Control {
private state: any;
}
// SelectableControl inherits the private state from Control
interface SelectableControl extends Control {
select(): void;
}
// Ok because SelectableControl is checking if Button has a private state
// originating in Control, which it does because Button extends Control
// but Button has no direct access to Control's private state, yet
// what SelectableControl checks is whether Button has private state
// in its prototypical chain, and if it originates in Control, which
// it does.
class Button extends Control implements SelectableControl {
select() {}
}
// TextBox inherits the private state only Control has access to it though
class TextBox extends Control {
select() {}
}
// Error - property state is missing in type 'Image'
// The problem here is that SelectableControl does not find
// a private state originating in Control, therefore it crashes
// even though Image has a private member state, just not originating
// in the same declaration as SelectableControl takes it from (Control).
class Image implements SelectableControl {
private state: any;
select() {}
}
class Location {
}
Only descendants of Control
will have a state
private member that originates in the same declaration, which is a requirement for private members to be compatible.
Within the Control
class it is possible to access the state
private member through an instance of SelectableControl
.
Intersection types
So if you have an intersection between two types, basically you have a narrower type, but with broader properties:
type Duck = Quackable & Swimable; // Duck has the union of Quackable and Swimable properties
A duck is only a duck if it can both quack and swim. If it' can only do one of the two, it is not a duck. What you get is the union of all properties, which is counterintuitive, but since there are more properties, less objects conform to it which makes it narrower...
Union types
On the other hand, if you have a union between two types, basically you have a broader type, but narrower set of property requirements:
type Flyable = Eagle | Butterfly; // Flyable has the intersection of Eagle and Butterfly
An Eagle can fly and pray, and the butterfly can fly and float. The intersection of the properties is the fly property. The union of these is therefore a smaller property set, thus a broader set of matching objects.
Custom type guards
Let's say you do not want to cast a type, but there is some custom logic that you require to know whether some object belongs to a type. So you can do something like this:
const canFly = (animal: Animal): animal is Flyable => typeof (animal as any).fly === 'function'
if (canFly(myAnimal)) {
animal.fly();
}
with the above, you can tell typescript that it can assume that this is actually a flyable. It is kind of a safer cast, but it depends on how safe your implementation of this function is. Another example.
funciton isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
})
pet is Fish
is our type predicate in this example. A predicate takes the form of parameterName is Type
where parameterName
must be a parameter name from the current function's signature. Any time isFish
is called with some variable as in:
if (isFish(pet)) {
pet.swim()
} else {
pet.fly()
}
typescript will narrow that variable down to that specific type, if the variable's original type is compatible.
Note that typescript not only knows that pet
is a Fish
in the if
branch; it also knows that in the else branch, if you don't have a Fish
then it must be a Bird
(Because it would have thrown a compilation error if the parameter passed to isFish() did not belong to the union type: Fish | Bird
).
Using the in
operator
The in
operator acts as a narrowing expression for types.
In the n in x
expression, n
is a string literal and x is a union type.
function move(pet: Fish | Bird) {
if ("swim" in pet) {
return pet.swim();
}
return pet.fly();
}
typeof typeguards
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string of number got ${padding}`);
}
You actually don't need to write a function to let Typescript know that you are typeguarding. You can write them inline. The only requirement is that the typenames you compare to are in: "number"
, "string"
, "boolean"
, or "symbol"
.
instanceof type guards
instanceof
is a way to narrow down a type by the constructor function. For example:
interface Padder {
getPaddingString(): string;
}
class SpaceCountPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString(): string {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString(): string {
return this.value;
}
}
function getRandomPadder() {
return Math.random() > 0.5 ? new SpaceCountPadder(5) : new StringPadder("---");
}
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceCountPadder) {
padder; //constructed with SpaceCountPadder
}
if (padder instanceof StringPadder) {
padder; //constructed with StringPadder
}
The right side of instanceof
must be a constructor function and typescript will narrow down to:
- the type of the function's
prototype
property if its type is notany
- the union of types returned by that type's construct signature in that order.
Nullable types
Typescript has two special types null
and undefined
that have the values null
and undefined
respectively. By default, the type checker considers these two types assignable to anything. Effectively, null
and undefined
are valid values of every type. That means that it si not possible to stop them from being assigned to variables of any type. The inventor of null
Tony Hoare calls this his billion dollar mistake.
That is why typescript has the --strictNullChecks
, when you declare a variable, it's type does not include null or undefined, which means you cannot assign them unless you explicitly include them via a union.
let s = "hello";
s = null; // error : 'null' is not assignable to 'string'
let sn: string | null = "world";
sn = null; // works
sn = undefined; // error : 'undefined' is not assignable to 'string | null'
Note: TypeScript treats null
and undefined
as different in order to match JavaScript semantics. So string | null
is different than string | undefined
and string | null | undefined
.
With --strictNullChecks
an optional parameter automatically adds | undefined
:
function f(x: number, y?: number): number {
if (y === undefined) {
return x;
}
return x + y;
}
f(1, 2); // works
f(1); // works
f(1, undefined); // works
f(1, null); // error: 'null' is not assignable to 'number | undefined'
The same is true for optional properties.
class C {
a: number;
b?: number;
}
let c = new C;
c.a = null; // error : 'null' is not assignable to 'number'
c.a = unefined; // error : 'undefined' is not assignable to 'number'
c.b = null; // error : 'null' is not assignable to 'number'
c.b = undefined; // works
Type guards and type assertions
Since nullable types are implemented with a union, you need to use typeguard to get rid of the null.
function f(sn: string | null): string {
if (sn == null) {
return "default"
}
return sn;
}
The null
elimination is pretty obvious here, but you can use terser operations:
function f(sn: string | null): string {
return sn || "default";
}
! for known non-null
In cases where the compiler can't eliminate null
or undefined
, you can use type assertion operator to manually remove them. The syntax is postfix !
: identifier!
removes null
and undefined
from the type of identifier
. In the example below, children[0]
could potentially be undefined
if children
is empty for example. So what we do with the !
is tell typescript that children[0]
's type does not contain undefined
or null
. And so it will contain the rest of types with which it was defined.
person.children[0]!.name; // here the developer tells ts that there will always be an element within the children array.
Another example using nested functions. We use nested functions because the compiler does not know what the value of the outer function's param will be before it is being called. So the compiler cannot eliminate nulls inside a nested function except if it is an IIFE which it isn't here :
function broken(sn: string | null): string {
function postFix(epithet: string) {
return sn.charAt(0) + '. the' + epithet; // error: 'sn' is possibly 'null'
}
sn = sn || "Bob";
return postFix("Great");
}
function fixed(sn: string | null): string {
function postFix(epithet: string) {
return sn!.charAt(0) + '. the' + epithet; // works
}
sn = sn || "Bob";
return postFix("Great");
}
Literal Types
You can actually use values as types. So if types are sets, a type defined with 3 values, is just the set of 3 values that conform to the type.
type Move = 'ROCK' | 'PAPER' | 'SCISSORS';
Type string
is the set of all strings, and type Move
above is just the set of these 3 strings. So:
let a: Move = 'HEY'; // Error: cannot assign 'HEY' to type Move
let b: Move = 'PAPER'; // Works!
Never
The never
type represents the type of values that never occur. For instance, never
is the return type for a function expression that will always throw an exception or one that never returns (like an infinite loop).
function myError(message: string): never {
throw new Error(message);
}
function fail() {
return myError('Something failed'); // inferred return type is never
}
function loop(): never {
while (true) {
console.log("this will never stop");
}
}
never
is the return type of a function that does not return. For example if you throw an error or you loop, that is a never
const throws = (): never => {
throw new Error('this never returns');
}
const loops = (): never => {
while (true) {
console.log('a piece of eternity, this function never returns');
}
}
Unknown
unknown
is a special type that forces you to do castings before being able to be assigned to a variable of another type.
Use unknown
for example when a user provides a value which you do not know the type in advance.
let a: string;
let x: any;
a = x; // any assigned to a string compiles
x = a; // string assigned to any compiles
let y: unknown;
y = a; // unknown accepts string, compiles
a = y; // Error: string does not accept unknown, NOT COMPILES
Index Types
keyof MyType
gives the set of all key types. From there you can retrieve the type of a specific key. And with all the keys you can get the whole union of types.
type Duck = {
color: string;
feathers: number;
}
type DuckProps = keyof Duck; // = 'colors' | 'feathers'
type ColorType = Duck['color']; // = string
type DuckValues = Duck[DuckProps]; // = string | number
Conditional Types
Depending on T
, it will resolve to a type or another one. In the example below, if we pass a a boolean type to the variable type in StringOrNumber
then we get a string
type.
type StringOrNumber<T> = T extends boolean ? string : number;
type T1 = StringOrNumber<true>; // string
type T2 = StringOrNumber<false>; // string
type T3 = StringOrNumber<object>; // number
This is a small example to get the type name of a specific type:
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"
The TypeName of a string is the string "string"
Infer
infer
is a special keyword that you can use within a conditional type.
type ElementType<T> = T extends (infer U)[] ? U : never;
type T0 = ElementType<[]>; // = never
type T1 = ElementType<string[]>; // = string
Here we are using infer
which is used to the right side of extends
. What it means is : if T
is an array of something, infer the type of that something (U) and resolve to that something (U); otherwise resolve to never.
T0
resolves to never
because T
is an array of nothing since it has no arguments.
T1
on the other hand.
Misc Patterns
Functional Programming
There is a difference between data and an object. The data is just pure data, whereas the object usually has methods. But the reality is we don't actually need objects, we only need functions that perform operations on those data structures. The cool thing is that you don't need to define any classes or constructors for data structures, you just need to define what kinds of values are there.
In the example below, typescript will tell you if you do any mistake (meaning that you don't need any constructors or any class etc), only the function that transforms one structure into the next.
interface RawPerson {
identifier: number;
first_name: string;
last_name: string;
}
interface Person {
id: string
fullName: string;
}
const transformPerson = (raw: RawPerson): Person => {
return {
id: `${raw.identifier}`,
fullName: `${raw.first_name} ${raw.last_name}`,
};
}
Discriminate Unions
So we have talked about unions, this is how unions look like. What the union does, is for common properties, it will unite the property values.
type Eagle = {
kind: 'eagle';
fly: () => 'fly';
};
type Duck = {
kind: 'duck';
quack: () => 'quack';
};
type Bird = {
kind: 'bird';
};
type Animal = Eagle | Duck | Bird; // If the object has any of the above props
let a: Animal = {kind: 'a', }; // Error : Type '"a"' is not assignable to type '"eagle" | "duck" | "bird"'
let b: Animal = {kind: 'bird', }; // b is of type Bird
b.quack(); // Error: property quack() does not exist on type 'Bird'
How can you discriminate unions?
const doSomething = (animal: Animal): string => {
switch (animal.kind) {
case "eagle":
return animal.fly();
break;
case "duck":
return animal.quack();
break;
default:
return assertNever(animal);
}
};
const assertNever = (animal: Animal): never => {
throw new Error(`unknown Animal, ${animal.kind}`);
}
In the above example, if we had not added a default
then the doSomething
return type should have been string | void
because the "bird"
case
would not be handled, and so anytime a Bird
was passed, doSomething
would return undefined
. To solve this, we can add the default
case which makes sure that when an Animal
not in the switch case
list is passed (such as Bird
) an error will be thrown. This is achieved returning a call to our assertNever
which has return type never
meaning it throws every time. Since never
is no return, we can keep the string
return type in the doSomething
.
Derive Types from Constants
const MOVES = {
ROCK: { beats: 'SCISSORS', },
PAPER: { beats: 'ROCK', },
SCISOR: { beats: 'PAPER', },
};
// Lets say we want to define the type Move which is the set of strings: "ROCK" | "PAPER" | "SCISSORS"
type MoveBad = keyof MOVES; // any 'MOVES' refers to a value, but is being used as a type here.
// the above does not work that is why we need to add a `typeof` keyword which gets the type of the moves constant
// and then retrieve the keys of the type (typeof MOVES), we do that with the help of `keyof`
type Move = keyof typeof MOVES;
const move: Move = 'ROCK';
Untrusted user input
If something is unknown you want perform some kind of validation, and make the unknown
type comply to some known type and return that. In the following example we are using the typeof
keyword which works like in javascript.
const validateInput = (s: unknown): number => {
let n;
switch (typeof s) {
case 'number':
// handle and return a number
break;
case 'string':
// handle and return a number
break;
default:
throw new Error('This kind of input is not supported');
}
}
Mapped types
Readonly
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyDuck = Readonly<Duck>; // { readonly color: string; readonly feathers: number; }
Partial
type Partial<T> = { [P in keyof T]?: T[P] };
type PartialDuck = Partial<Duck>; // { color?: string; feathers?: number; }
Required
type PartialDuck = {
color?: string;
feathers?: number;
}
type Required<T> = { [P in keyof T]-?: T[P] };
type RequiredDuck = Required<PartialDuck>; // { color: string; feathers: number; }
Pick
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
type ColorDuck = Pick<Duck, 'color'>; // { color: string; }
Pick SQL
async function fetchPersonById<T extends keyof Person> (
id: string,
...fields: T[]
): Promise<Pick<Reaction, T>> {
return await connection('Person')
.where({ id })
.select(fields)
.first();
}
const id = 'asdfasdf';
const reaction = await fetchPersonById(id, 'name', 'age'); // = { name: string, age: number }
Generics
Generics are here to allow you to have components that can work on a variety of types rather than a single one. This allows users to consume the components with their own types.
Hello World of Generic
The identity function is a good example for the generic type. The identity function is like in matrix algebra, a function that returns the input itself. We could achieve this in two ways without generics:
-
Adding a type to the input and the same to the return. Which restricts the possible inputs.
function identity(input: number): number { return input; }
-
Use the
any
type to allow any type. This is generic, but it will make us lose information about the input type: when the function returns, whatever type the input was, typescript will now have unconstrained it toany
.function identity(input: any): any { return input; }
Instead, with generics we can allow both:
- Allowing any input type
- Not loosing the input's type on return
function identity<T>(input: T): T {
return input;
}
Notice the <T>
is a type variable, when calling the identity function, the user can specify the type he wants that variable to hold explicitly:
const s = identity<string>("Hello");
Above you can see that the user specified that the type variable T
should hold the type string
. But we can also let the compiler figure out the type implicitly the type of the argument:
const s = identity("Hello");
The above uses type argument inference, where the compiler sets the value of T
, here it will be set to string
by the compiler.
Argument's shape
Typescript needs to know what are the available methods of the generic argument before you can use them. Otherwise it will complain that T
does not have the specified method like below:
function consoleLogIdentity<T>(input: T): T {
console.log(input.length); // Error: T does not have .length
return input;
}
Because before consoleLogIdentity
is called, there is no way for TypeScript to know if input of unknown type T
has any method at tall.
Let's say we have intended the consoleLogIdentity
function to be called on arrays of what the T
variable type will hold when it is called. We can then define it as:
function consoleLogIdentity<T>(input: T[]): T[] {
console.log(input.length);
return input;
}
input
is now of type Array of some type. So the .length
prop is available and the code logs the number of elements of the array. An alternative writing for this is:
function consoleLogIdentity<T>(input: Array<T>): Array<T> {
console.log(input.length);
return input;
}
This is the same as the same as the previous code just with a different notation.
Generic Interfaces
Previously we created generic identity functions that worked over a range of types. Now we will explore the type of the functions themselves and how to create generic interfaces. The type of generic function is just like those of non-generic functions, with the type parameters listed first, similarly to function declarations:
function identity<T>(input: T): T {
return input;
}
let myIdentity: <U>(inp: U) => U = identity;
Above, note that the name of the variable need not match. T
or U
does not matter so long as the number of type variables and how the are lined up match.
We can also write the generic type as a call signature of an object literal type:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity = {<T>(arg: T): T} = identity;
Which leads us to writing our first generic interface. Let's take the object literal from above and move it to an interface:
interface GenericIdentityFn {
<T>(input: T): T
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
We may also want to move the generic parameter to be a parameter of the whole interface like so:
interface GenericIdentityCustomType<T> {
(arg: T): T;
}
function identity<T>(input: T): T {
return input;
}
let myNumIdentity: GenericIdentityCustomType<number> = identity;
myNumIdentity
now only accepts number
as first argument, we have "casted" identity
to accept only type number
as first argument. So myNumIdentity
is not a generic at all.
Generic Classes
Generic classes have a similar shape as generic interfaces. Generic classes have a generic type parameter list in angle brackets following the name of the class:
class GenericSet<T> {
zeroValue: T;
add: (x: T, y: T) => T;
// add: (x: T, y: T) {}
}
let integer = new GenericSet<number>();
integer.zeroValue = 0;
integer.add = function(x, y) { return x + y; };
This is a pretty literal use of the GenericSet
class. But nothing stops us from creating a set for strings:
let str = new GenericSet<string>();
str.zeroValue = "";
str.add = function(s1, s2) { return s1 + s2; };
console.log(str.add(str.zeroValue, "Something else"));
Note: Generic classes can work only on the instance part of the class.
Type Inference
Type is what Typescript does when no explicit type annotation. In its simplest form:
let x = 2;
Here typescript infers that x
has type number
.
Best common type
The best common type arises in cases where the type needs to be inferred from a set of expressions like in:
let a = [1, 2, null];
Here typescript will look at the set of types within the array: number
, number
and null
. So there is a common type between the two first elements (number
), but third type is null
which is not within the number
type. In cases where typescript finds variables with different types, it will create a union, here: number | null
.
Typescript will not go as far as to find a common ancestor:
let zoo = [new Rhino, new Giraffe, new Lion]; // [ts] zoo: Rhino | Giraffe | Lion
Even though all objects extend from Animal
, typescript will use the union of each class instead. To force using the Animal
type you need to be explicit:
let zoo: Animal[] = [new Rhino, new Giraffe, new Lion]; // [ts] zoo: Animal[]
Contextual typing
Is type inference but instead of going from right to left, it goes from left to right:
window.onmousedown = function(ev) {
console.log(ev.button); // ok
console.log(ev.someMethodNotBelongingToMouseEvent); // [ts] Error (not when I tried, contextual typing didn't work for me)
}
A better example is again with the zoo where the return type has a few candidates from the return expression plus the return type annotation:
function createZoo(): Animal[] {
return [new Rhino(), new Crocodile(), new Loris()];
}
Here the contextual typing inferences has 4 candidates for the return type: Animal
, Rhino
, Crocodile
and Loris
, which all have a common type ancestor: Animal
. Therefore Animal
is finally chosen by typescript.
Type Compatibility
Type compatibility in TypeScript is based on structural subtyping: a way of relating types based on their members. The rules is:
x
is compatible withy
ify
has at least the same members asx
.
IMPORTANT: structural typing is different from duck typing. Structural typing is a static typing system that determines type compatibility and equivalence by the type's structure. Whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time.
Sound, Unsound, Soundness, Unsoundness
Soundness means that if the type system says that a variable has a particular type, then it definitely has that type at runtime. A sound system is always correct, and an unsound type system might be incorrect in some cases.
function pushANumber(a: (string | number)[]): void {
a.push(100);
}
const strings: string[] = ["one", "two"];
pushANumber(strings);
const myFakeString: string = strings[strings.length]
myFakeString.toLowerCase(); // [ts] ok but ;; [js] Uncaught TypeError: myFakeString.toLowerCase is not a function
In the above code, typescript is unable to detect that myFakeString
is actually a number (here 100
). So typescript is unsound.
TypeScript's type system allows certain operations that can't be known at compile time to be safe. When a type system has this property, it is said to not be "sound".
The places where typescript allows unsound behavior where carefully considered and trhought this document we'll explain where these happen and the motivating scenarios behind them.
interface Named {
name: string;
}
let x: Named;
const john = {name: "john", sex: "male"};
x = john; // works
function callName(n: Named): void {
console.log(n.name);
}
callName(john); // works
Typescript will check each property of x
and make sure there is a corresponding compatible property in john
. Which there is so ti works. It works exactly the same with function parameters, where in callName
: n
takes the place of x
, so typescript checks whether each property of n
is present in the supplied param john
.
Comparing two functions
Comparing primitive and object types is pretty straightforward.
Checking the compatibility of functions is more involved.
let one = (a: number) => a + "";
let two = (b: number, s: string) => a + s;
one = two; // [ts] Type '(a: number, s: string) => number' is not assignable to type '(a: number) => number'
two = one; // [ts] ok
two(1, "2"); // [ts] ok but parameter 2 is ignored in one
two(1); // [ts] Error! Expected 2 arguments, but got 1
You can see that one
is assignable to two
but the reciprocal is not true. Why? Because in javascript, ignoring extra function parameters is quite common, like Array#forEach
:
Even though two
is assigned a one
, the type of two
remains the same, meaning that you must still call it with 2 parameters even though the function one
does not use the second parameter. So the reduced parameter call is not allowed within typescript. See the Array#forEach
example:
let items = [1, 2, 3];
// passing a fully defined callback
items.forEach((item, index, array) => console.log(item));
// passing a callback with only those parameters in use
items.forEach((item) => console.log(item));
So to think back of our previous example above, with one
and two
, in the second items.forEach()
call we are passing a one
(the callback) into a two
(the foreach parameter).
The key point here, is that we are assigning a function to a variable like before one
to two
(but here the two
is the forEach parameter). Remeber that we were not allowed to call two(1)
with a single parameter, so how is forEach
going to do it? It does so out of typescript's reach, inside the V8.
Comparing function return types
Here we are going to compare two functions where the returned objects differ in one having a member that the other does not have, but the both share a common member:
let retOne = () => ({ common: "he"});
let retTwo = () => ({ common: "di", onlyHere: "lo"})
retOne = retTwo; // [ts] Ok because retOne's users do not risk calling unavailable props since retTwo has all of retOne's
retTwo = retOne; // [ts] Type '() => { common: string; }' is not assignable to type '() => { common: string; onlyHere: string; }'. Property 'onlyHere' is missing in type '{ common: string; }' but required in type '{ common: string; onlyHere: string; }'
In conclusion, function compatibility is computed oppositely for the parameter list and the return types:
- subset parameter lists can be assigned to superset ones (reciprocal false).
- returned object with superset of members can be assigned to a subset (reciprocal false)
IMPORTANT: the type of the variable we are assigning to, does not change after the assignment of a different function shape. So it is still mandatory to provide all parameters of the original variable function type.
Functional Parameter Bivariance
When comparing function parameter lists, assignment succeeds if there is a common denominator between the target and source parameters, it works both ways: CYBER20
Decorators
It is just a functionality that allows you to hook into your code and either extend the functionality of it, or annotate it with metadata. So why would you want to annotate your code with metadata? The answer is you probably don't. There might be some good use cases, but it come more into play if you are building something like a compiler (the Angular compiler for example) and you need to analyse the metadata to do something like dependency injection. So decorators ca provide this metadata, but the can also hook directly into your code and alter the behavior of it. And that part is what is really useful to the average app developer because it allows to write abstractions that is clear and concise. But decorators are almost too good at creating abstractions, so you must be careful not to overuse them. You could be tempted to create a decorator for every little thing, but they should be used for logic that is stable and that needs to be reused frequently throughout the app.
What can you decorate?
- Class definitions
- properties
- methods
- accessors
- parameters
For example, lets take an Angular component that we want to prevent from being extended:
// Create a function that will simply make the constructor readonly
function Frozen(constructor: Function) {
Object.freeze(constructor);
Object.freeze(constructor.prototype);
}
@Frozen
export class IceCreamComponent {
}
// console.log(Object.isFrozen((IceCreamComponent));
class Froyo extends IceCreamComponent {} // [js] Uncaught TypeError: Cannot assign to read only property constructor of object
When extending classes with decorators, you need to be careful because the extending class will not recieve the decorator functionalities from the parent.
The most useful decorators are those of properties and methods.
For example let's say we have a flavor
property and every time there is such property, we want to add an emoji to the beginning and to the end of it. (Notice that we are calling our decorator with parenthesis @Emoji()
. This is because we are using the decorator as a factory, allowing us to pass parameters if needed).
This may sound trivial, but we are actually hooking into the getting and setting of this property which could be very powerful.
We will also create a method decorator to the addTopping
method. Note that decorators are composable, so we can stack them on top of each other and they will be executed from top to bottom.
export class IceCreamComponent {
@Emoji()
flavor = 'vanilla';
toppings = [];
@Confirmable('Are you sure?');
@Confirmable('Really? Are you super sure?');
addTopping(topping = 'sprinkles') {
thisl.toppings.push(topping);
}
}
The key
is the name of the property we want to decorate on the target
object.
function Emoji() {
return function(target: Object, key: string | symbol) {
let val = target[key];
const getter = () => {
return val;
};
const setter = (next) => {
console.log('Updating flavor...');
val = `EMOJI${next}EMOJI`;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
}
function Confirmable(message: string) {
return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
const original = descriptor.value;
// redefine the method property function by wrapping it with a confirmation
// but instead of executing the original directly, present a confirm
// dialog with the message. And if the confirm returns true, then execute
descriptor.value = function(...args: any[]) {
const allow = confirm(message);
let result = null;
if (allow) {
// apply the original function with the original arguments
result = original.apply(this, args);
}
return result;
};
}
}
Above you see that the decorator function should take different parameters depending on what it is supposed to decorate:
- for property decorators, the function signature is:
function(target: Object, key: string | symbol)
- for method decorators the function signature is:
function(target: Object, key: string | symbol, descriptor: PropertyDescriptor)
, wheredescriptor
is the actual method itself.
Let's try another example with an accessor. Let's say a getter's role is to calculate the price:
export class IceCreamComponent {
// ...
basePrice = 5;
@WithTax(0.15);
get price() {
return this.basePrice + 0.25*this.toppings.length;
}
}
One thing you are not allowed with javascript getters, is to pass an argument like price(0.15) //Error
, so instead we can use a decorator to add the tax for us.
function WithTax(taxRate) {
return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
let original = descriptor.get;
descriptor.get = function() {
const result = original.apply(this);
return (result * (1 + taxRate)).toFixed(2)
}
return descriptor;
}
}
Now let's create a final example where we will implement the React useState
hook using decorators:
Here is what we want to mimic from React:
import {useState, useEffect} from 'react';
function Example() {
let [count, setCount] = useState(0);
// similar to componentDidMount and componentDidUpdate
useEffect(() => {
// update the document title using browser API
document.title = `You pressed ${count} times`;
});
return {
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
};
}
So using Angular we would need to do the following to get the exact same behavior:
import { Component } from '@angular/core';
// Using an angular built-in decorator to create a component
@Component({
selector: 'app-hooks',
template: `
<p>You clicked [{count}] times</p>
<button (click)="setCount(count + 1)">
Click me
</button>
`,
});
export class HooksComponent {
// This is our property decorator
@UseState(0) count; setCount;
// and a method decorator that will run an
// effect either when the component is initialized
// or when any value changes in the component
@UseEffect()
onEffect() {
document.title = `You clicked ${this.count} times`;
}
}
// property decorator so has target and key
function useState(seed: any) {
return function(target: Object, key: string | symbol) {
target[key] = seed;
target[`set${key.replace(/^\w/, c => c.toUpperCase())}`] = function(val) { target[key] = val; };
};
}
function UseEffect() {
return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
target.ngOnInit = descriptor.value;
target.ngAfterViewChecked = descriptor.value;
}
}