TypeScript Reference Guide
Table of Contents generated with DocToc
Fundamentals
- Typescript is a types, syntactic superset of JavaScript, developed by Microsoft
- Compiles to JavaScript and doesn’t have its own runtime, i.e. can be thought of as a static type analysis tool
- TypeScript uses a structurally typed system (focused on the shape that values have) as opposed to languages
like Java and C++ that use a nominally typed system (focused on the name that values have)
class Foo { method(input: string) { /* ... */ } } class Bar { method(input: string) { /* ... */ } } let foo: Foo = new Bar(); // 🚨 Error in a nominally typed language, but OK in a structurally typed language
- To show inferred type in WebStorm:
Command + hover on variable
- Compile typescript code
# Compiles to ES3 target, i.e. JS that is compatible with IE6 tsc src/index.ts # Compiles to ES6 (ES2015) tsc src/index.ts --target ES2015 # Compules to ES2015 and uses commonjs (used by node) modules tsc src/index.ts --target ES2015 --module commonjs
- Ref: TS Compiler Options
- Conventional way to set compiler flags is to use a file called tsconfig.json
- Example
{ "compilerOptions": { "module": "commonjs", "target": "es2017", "outDir": "lib", "declaration": true, "sourceMap": true }, "include": ["src"] }
- Example
- Utility for testing types: https://github.com/Microsoft/dtslint
Basic Types
- Variables with type
any
let z; // Variable 'z' implicitly has an 'any' type, but a better type may be inferred from usage. z = 41; z = "abc"; // we could improve this situation by providing a type annotation when we declare our variable let zz: number; zz = 41; zz = "abc"; // 🚨 ERROR Type 'abc' is not assignable to type 'number'.
- Array type
let aa: number[] = []; aa.push(33); aa.push("abc"); // 🚨 ERROR: Argument of type '"abc"' is not assignable to parameter of type 'number'. // The following means bb is an array of `never` and hence no elements of any type can be pushed to the array let bb = []; bb.push(44); // 🚨 ERROR: Argument of type '33' is not assignable to parameter of type 'never'.
- Tuple (array of fixed length) type
let cc: [number, string, string, number] = [ 123, "Fake Street", "Nowhere, USA", 10110 ]; // we don't get any type safety when using push with a tuple and hence the below should be avoided with a tuple // in this example, the push method accepts element whose type matches `string | number` cc.push(1, 2, 3, 4);
- Tuple values should always have a type annotation so that the type system doesn’t consider the variable as a regular
array
const xx = [32, 31]; // type: number[]; const yy: [number, number] = [32, 31]; // type: [number, number]
- Object types
let cc: { houseNumber: number; streetName: string }; cc = { streetName: "Fake Street", houseNumber: 123 }; cc = { houseNumber: 33 }; /** * 🚨 Property 'streetName' * 🚨 is missing in type '{ houseNumber: number; }' * 🚨 but required in type '{ houseNumber: number; streetName: string; }'. */
- Object types with optional params
let dd: { houseNumber: number; streetName?: string }; dd = { houseNumber: 33 };
- Interface types
interface Address { houseNumber: number; streetName?: string; } let ee: Address = { houseNumber: 33 };
Function Basics
- Params and return values can have type annotations
interface HasPhoneNumber { name: string; phone: number; } interface HasEmail { name: string; email: string; } function sendEmail(to: HasEmail): { recipient: string; body: string } { return { recipient: `${to.name} <${to.email}>`, body: "Hello, world!" }; }
- Overloaded function signatures
interface HasPhoneNumber { name: string; phone: number; } interface HasEmail { name: string; email: string; } function contactPeople(method: "email", ...people: HasEmail[]): void; function contactPeople(method: "phone", ...people: HasPhoneNumber[]): void; // "function implementation" function contactPeople( method: "email" | "phone", ...people: (HasEmail | HasPhoneNumber)[] ): void { if (method === "email") { (people as HasEmail[]).forEach(sendEmail); } else { (people as HasPhoneNumber[]).forEach(sendTextMessage); } } // ✅ email works contactPeople("email", { name: "foo", email: "" }); // ✅ phone works contactPeople("phone", { name: "foo", phone: 12345678 }); // 🚨 mixing does not work contactPeople("email", { name: "foo", phone: 12345678 });
- To prevent confusion over what
this
might be bound to, it is best to always explicitly specify the type ofthis
argument the function expects - Additionally, the
--noImplicitThis
warns if the type ofthis
isany
. Example:// With the `--noImplicitThis` compiler flag set function sendMessage( preferredMethod: "phone" | "email" ) { if (preferredMethod === "email") { console.log("sendEmail"); sendEmail(this); // 🚨 'this' implicitly has type 'any' because it does not have a type annotation. } else { console.log("sendTextMessage"); sendTextMessage(this); // 🚨 'this' implicitly has type 'any' because it does not have a type annotation. } }
- Explicitly specifying the type of
this
function sendMessage( this: HasEmail & HasPhoneNumber, preferredMethod: "phone" | "email" ) { if (preferredMethod === "email") { console.log("sendEmail"); sendEmail(this); } else { console.log("sendTextMessage"); sendTextMessage(this); } }
- With the type of
this
explicitly specified, the compiler points out any scope binding issues// ...sendMessage as defined in the previous snippet // 🚨 The 'this' context of type 'void' is not assignable to method's 'this' of type 'HasEmail & HasPhoneNumber'. sendMessage("email"); // ✅ creating a bound function is one solution sendMessage.bind(c, "email"); // ✅ call/apply works as well sendMessage.apply(c, ["phone"]);
Interfaces and Type Aliases
- Type aliases allow us to give a type a name
type StringOrNumber = string | number; type HasName = {name: string};
- Types don’t allow self references as the TS compiler evaluates types, inline/eagerly as it parses through the code line by
line as opposed to interfaces which are evaluated lazily
// 🚨 self-referencing types don't work! type NumVal = 1 | 2 | 3 | NumArr; // type NumArr = Numval[]; // Cannot find name 'Numval'. // ✅ this can be made to work by using an interface interface NumArr extends Array<NumVal> {} const x: NumVal = [1, 2, 3, 1, 1, [3, 1, 1, 2]]; // 🚨 Initializer type 4 is not assignable to variable type NumVal const y: NumVal = 4;
- Interfaces can extend from other interfaces
interface HasPhoneNumber { name: string; phone: number; } interface HasInternationalPhoneNumber extends HasPhoneNumber { countryCode: string; }
- Readonly properties
interface Point { readonly x: number; readonly y: number; } let p1: Point = { x: 10, y: 20 }; p1.x = 5; // 🚨 Attempt to assign to const or readonly variable
- Readonly arrays
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error! ro.push(5); // error! ro.length = 100; // error! a = ro; // error!
- Readonly is like const, but for object properties
- Interfaces can describe objects, functions, arrays, i.e. everything that is an object (functions and arrays are objects in JS), but not primitives
- Type aliases are flexible and can handle everything interfaces handle plus primitives
- Typing function signatures
// using an interface interface ContactMessenger1 { (contact: HasEmail | HasPhoneNumber, message: string): void; } // equivalent signature type using a type alias type ContactMessenger2 = ( contact: HasEmail | HasPhoneNumber, message: string ) => void; // using a function signature const emailer: ContactMessenger1 = (_contact, _message) => { /** ... */ };
- Typing constructor signatures
interface ContactConstructor { new (...args: any[]): HasEmail | HasPhoneNumber; }
- Excess property checks: if an object literal has any properties that the “target type” doesn’t have, you’ll get an
error, but this doesn’t apply to object variables
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... return {color: "bla", area: 100}; } // 🚨Argument type {colour: string, width: number} is not assignable to parameter type SquareConfig let mySquare = createSquare({ colour: "red", width: 100 }); // ✅ Excess property checks only apply to object literals and not object variables let mySquareConfig = { colour: "red", width: 100 }; mySquare = createSquare(mySquareConfig);
- Index types
interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0]; interface NumberOrStringDictionary { [index: string]: number | string; length: number; // ok, length is a number name: string; // ok, name is a string } let myDict : NumberOrStringDictionary = {length: 1, name: "abc"}; myDict = {length: 1}; // 🚨Assigned expression type {length: number} is not assignable to type NumberOrStringDictionary
- Declaration merging - interfaces are “open”, meaning any declarations of the same name are merged
interface PhoneNumberDict { [thisCanBeAnythingItsIgnoredOnlyTypeIsImportant: string]: | undefined | { areaCode: number; num: number; }; } interface PhoneNumberDict { home: { areaCode: number; num: number; }; office: { areaCode: number; num: number; }; } const phoneDict: PhoneNumberDict = { // office and home are mandatory because of the 2nd interface declaration office: { areaCode: 321, num: 5551212 }, home: { areaCode: 321, num: 5550010 }, // this is allowed due to the first interface declaration iphone: { areaCode: 321, num: 5550010 } }; phoneDict.home; // definitely present phoneDict.office; // definitely present phoneDict.mobile; // MAYBE present
Classes
- Classes implement interfaces and can use parameter properties shorthand
interface HasEmail { name: string; email: string; } class Contact implements HasEmail { email: string; name: string; constructor(name: string, email: string) { this.email = email; this.name = name; } } // This is a short-hand using `parameter properties` that is exactly same as the previous more verbose definition class ParamPropContact implements HasEmail { constructor(public name: string, public email: string) { // nothing needed } }
Generics
- Parameterize types
interface WrappedValue<X> { value: X; } let val: WrappedValue<string> = { value: "" };
- You don’t have to use exactly your type parameter as an arg, instead things that are based on your type parameter are
fine too
function resolveOrTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> { return new Promise<T>((resolve, reject) => { // start the timeout, reject when it triggers const task = setTimeout(() => reject("time up!"), timeout); promise.then(val => { // cancel the timeout clearTimeout(task); // resolve with the value resolve(val); }); }); } // Resolves T as Response, based on argument `fetch` which returns a Promise<Response> resolveOrTimeout(fetch(""), 3000);
- Type params can use inheritance constructs
function foo<T extends { id: string }>(param1: T): T { //... return param1; }
- Type params are associated with scopes, just like function arguments
function startTuple<T>(a: T) { return function finishTuple<U>(b: U) { return [a, b] as [T, U]; }; } // type of `myTuple` is `[string[], number]` const myTuple = startTuple(["first"])(42);
- Its best to use generics when we want to describe a relationship between two or more types, example, a function
argument and return type
- Its an overkill to use Generics in the following scenario
type Shape = { draw(); }; interface Circle extends Shape { radius: number; } function drawShapes1<S extends Shape>(shapes: S[]) { return shapes.map(s => { s.draw(); }); } // Its simpler and equivalent to just use the following function drawShapes2(shapes: Shape[]) { shapes.forEach(s => s.draw()); }
- Its useful to use Generics in the following scenario, to link the type of function arg and its return type
type Shape = { draw(); isDrawn: boolean; }; interface Circle extends Shape { radius: number; } function drawShapes1<S extends Shape>(shapes: S[]): S[] { return shapes.map(s => { s.draw(); s.isDrawn = true; return s; }); } const cir: Circle = { draw() {}, radius: 4, isDrawn: false }; drawShapes1([cir]).map(c => c.isDrawn);
- Its an overkill to use Generics in the following scenario
Type Guards
- Typescript has two top types -
any
andunknown
unknown
s need to be narrowed using a type guard before they can be usedlet myUnknown: unknown = "hello, unknown"; if (typeof myUnknown === "string") { // in here, myUnknown is of type string myUnknown.split(", "); // ✅ OK } if (myUnknown instanceof Promise) { // in here, myUnknown is of type Promise<any> myUnknown.then(x => console.log(x)); }
- User defined type guards
function isHasEmail(x: any): x is HasEmail { return typeof x.name === "string" && typeof x.email === "string"; } if (isHasEmail(myUnknown)) { // In here, myUnknown is of type HasEmail console.log(myUnknown.name, myUnknown.email); } function isDefined<T>(arg: T | undefined): arg is T { return typeof arg !== "undefined"; } const list = ['a', 'b', undefined, 'c']; const filtered = list.filter(isDefined);
Advanced Types
- Mapped Types allow the use of an interface to transform keys into values
interface CommunicationMethods { email: HasEmail; phone: HasPhoneNumber; fax: { fax: number }; } function contact<K extends keyof CommunicationMethods>( method: K, contact: CommunicationMethods[K] // 💡turning key into value -- a *mapped type* ) { //... } contact("email", { name: "foo", email: "mike@example.com" }); contact("phone", { name: "foo", phone: 3213332222 }); contact("fax", { fax: 1231 }); // we can get all values by mapping through all keys type AllCommKeys = keyof CommunicationMethods; type AllCommValues = CommunicationMethods[keyof CommunicationMethods];
- Partial allows us to make all properties on on object optional
type MayHaveEmail = Partial<HasEmail>; const me: MayHaveEmail = {}; // everything is optional
Extract
andExclude
allows us to obtain a subset of types from given typestype OnlyStrings = Extract<"a" | "b" | 1 | 2, number>; // type OnlyStrings = "a" | "b" type NotStrings = Exclude<"a" | "b" | 1 | 2, string>; // type NotStrings = 1 | 2
References