Typescript tutorial
interface Point2D {
x: number;
y: number;
}
var point2D: Point2D = { x: 0, y: 10 }
function iTakePoint2D(point: Point2D) { /* do something */ }
Interface that accepts additional keys:
interface Model {
name: string;
[key: string]: any; // Or [key: string]: unknown;
[key: number]: any;
}
Note: We should have had a reserved word or built-in type like "base interface" which allows additional properties since it is a common use case.
let vAny: any = 10; // We can assign anything to any
let vUnknown: unknown = 10; // We can assign anything to unknown just like any
typeof vUnknown === 'number' // True.
let uk:unknown;
typeof uk === 'undefined' // True
let s1: string = vAny; // Any is assignable to anything
let s2: string = vUnknown; // Invalid; we can't assign vUnknown to any other type (without an explicit assertion)
vAny.method(); // Ok; anything goes with any
vUnknown.method(); // Not ok; we don't know anything about this variable
vUnknown = 'hello'; // OK to reuse "unknown" variable with another type object.
typeof vUnknown === 'string' // True.
enum Direction {
Up = 1, // By default first one gets value 0.
Down, // Automatically gets value 2 etc.
Left,
Right,
}
enum Direction { Up = "UP", Down = "DOWN" }
// Defines: Direction = { 0: "UP", 1: "Down", "UP":0, "Down": 1 }
const enum Enum { A = 1, B = A * 2 } // const enums are inlined and removed.
For quick and dirty migration for 3rd party library, you can do:
declare var $: any; // Entire jquery library, won't complain now. // You can include ambient type definition for 3rd party library // from DefinitelyTyped github repository JQuery.d.ts file.
Another way to deal with 3rd party npm module is to delcare that as module:
declare module "jquery";
import * as $ from "jquery";
// External non js resources
declare module "*.css";
// Now you can ...
import * as foo from "./some/file.css";
declare module "*.html";
Another even better way is to install type defintion from DefinitelyTyped repo:
npm install @types/jquery --save-dev
// By default the global definitions get polluted. $ is immediately available.
If you want to selectively enable global types, edit tsconfig.json:
{ "compilerOptions": { "types" : [ "jquery" ] } }
You can selectively import @types using module:
import * as $ from "jquery";
// Use $ at will in this module :)
Insert all your "declare ..." statements in global.d.ts and vendor.d.ts file. Every statement in this file should start with declare so that no code is emitted after compilation. Author has to make sure, it is available during runtime.
For example:
declare var process: any; // Or import community maintained node.d.ts
Typescript compiler comes with global lib.d.ts file which defines declration for DOM, for variables like window, document, math, etc.
You can target es5 but use es6 and dom for type assist :
tsc --target es5 --lib dom,es6
// tsconfig.json
"compilerOptions": { "lib": ["dom", "es6"] }
Polyfill for old JavaScript engines :
npm install core-js --save-dev
// Add following in your application entry point:
import "core-js";
$('.awesome').show(); // Error: cannot find name `$`
declare var $: any;
$('.awesome').show(); // Okay!
declare const fooSdk : { doSomething: () => boolean }
fooSdk.doSomething(); // fooSdk is global variable. Now Okay!
declare let module: any; // suppress errors for module.xxx access.
let module: any; // Error! module already defined!
Importing from javascript module:
// getMyName.js
module.exports = function getMyName(name) {
return name;
}
Then you import it into your typescript code
// myCode.ts
import getMyName from 'getMyName'; //getMyName is a JS module not typescript
When importing that JS module(allowJs compiler option should be set to false
otherwise, JS files will be resolved without any type definitions),
// Could not find a declaration file for module '/absolute/path/to/getMyName.ts'
// Provide type definition file now!
// getMyName.d.ts
declare function getMyName(name: string): string;
export default getMyName; // Now Okay!
interface Options {
x?: string;
y?: number;
}
// Error, no property 'z' in 'Options'
let q1: Options = { x: 'foo', y: 32, z: 100 };
// "as Options" is called type assertion.
// Forces to accept extra property present.
let q2 = { x: 'foo', y: 32, z: 100 } as Options;
// Still an error (good):
// Note: x is type incompatible. Type assertion does not tollerate it!
let q3 = { x: 100, y: 32, z: 100 } as Options;
type union:
// This is a dog or a cat or a horse, not sure yet
interface Animal { move; }
interface Dog extends Animal { woof; }
interface Cat extends Animal { meow; }
interface Horse extends Animal { neigh; }
let x: Animal;
if(...) {
x = { move: 'doggy paddle', woof: 'bark' };
} else if(...) {
x = { move: 'catwalk', meow: 'mrar' };
} else {
x = { move: 'gallop', neigh: 'wilbur' };
}
type intersection is similar to interface extends:
interface DataModelOptions {
name?: string;
id?: number;
}
interface UserProperties {
[key: string]: any;
}
function createDataModel(model: DataModelOptions & UserProperties) {
/* ... */
}
// OK. model additional properties accepted.
createDataModel({name: 'my model', favoriteAnimal: 'cat' });
// Note:
interface Client {
name: string;
address: string;
}
// We can express the same Client contract definition using type annotations:
type Client = {
name: string;
address: string;
};
// Note: type syntax involves using "=".
// Both above interface and type are synonymous.
type Address = string;
type NullOrUndefined = null | undefined; // Union type alias
type Transport = 'Bus' | 'Car' | 'Bike' | 'Walk'; // Like Enum type
type AddFn = (num1: number, num2:number) => number;
interface IAdd { (num1: number, num2:number): number; } // Similar to AddFn
// Note: AddFn looks better than IAdd.
// Generic (parameterized) Type:
type Car = 'ICE' | 'EV';
type ChargeEV = (kws: number)=> void;
type FillPetrol = (type: string, liters: number) => void;
type RefillHandler<A extends Car> = A extends 'ICE' ? FillPetrol :
A extends 'EV' ? ChargeEV : never;
const chargeTesla: RefillHandler<'EV'> = (power) => {
// Implementation for charging electric cars (EV)
};
const refillToyota: RefillHandler<'ICE'> = (fuelType, amount) => {
// Implementation for refilling internal combustion engine cars (ICE)
};
// Note: never is an impossible type.
interface Client { name: string; }
interface Client { age: number; } // Ok. Interface declarations are merged.
type Client = { name: string; }
type Client = { age: number; } // Error! type redefined!
// Below both interface and type are similar. extends vs intersection.
interface VIPClient extends Client { benefits: string[] }
type VIPClient = Client & {benefits: string[]};
// When extending interfaces, the same property key isn’t allowed.
// But type intersection has complex semantics.
type Person = {
getPermission: (id: string) => string;
};
type Staff = Person & {
getPermission: (id: string[]) => string[];
};
const AdminStaff: Staff = {
getPermission: (id: string | string[]) =>{
return (typeof id === 'string'? 'admin' : ['admin']) as string[] & string;
}
}
// Note: Type intersection creates union type for same property.
// You can implement class using interface or type. Both okay:
interface Person { name: string; greet(): void; }
class Student implements Person {
name: string;
greet() {
console.log('hello');
}
}
type Pet = { name: string; run(): void; };
class Cat implements Pet { ... }
// Tuple type is supported:
type TeamMember = [name: string, role: string, age: number];
const peter: TeamMember = ['Harry', 'Dev', 24];
// You can have static value in type:
type NetworkFailedState = {
state: "failed";
code: number;
};
type NetworkLoadingState = {
state: "loading";
};
...
type state = NetworkFailedState | NetworkLoadingState;
let myState:state;
// Okay. myState.code access is OK due to static code analysis!!!
if (myState.state === 'failed') console.log(myState.code);
See Also: https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html
// Type inference ...
const button = document.getElementById('my-element')
// Using type inference it assumes the button type is: HTMLElement | null
// You know that it is of type button element ...
const button = document.getElementById('my-element') as HTMLButtonElement
// You just know the result is not null. Then just add ! at the end:
const element = document.getElementById('my-element')!
// and element will have type HTMLElement.
interface Animal { move; }
interface Dog extends Animal { woof; }
interface Cat extends Animal { meow; }
interface Horse extends Animal { neigh; }
let x:Animal;
let y:Animal;
x = <Dog>y; // Typecast to Dog. Deprecated syntax due to JSX conflict.
x = y as Dog; // Preferred way for casting.
let k = 10;
let s = k as string; // Error! Types don't overlap for cast to succeed!
Note: Basically, the assertion from type S to T succeeds
if either S is a subtype of T or T is a subtype of S.
// Following function is a user defined type guard.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
// Using traditional alternative approach, you can do this ...
let pet = getSmallPet();
let fishPet = pet as Fish;
let birdPet = pet as Bird;
if (fishPet.swim) { // Or check: ((pet as Fish).swim)
fishPet.swim();
} else if (birdPet.fly) {
birdPet.fly();
}
// Using "in" Operator is another preferred way of type narrowing.
function move(pet: Fish | Bird) {
if ("swim" in pet) {
return pet.swim();
}
return pet.fly(); // Deducts if "swim" not there, "fly" must be there!
}
// Above is pretty cool and mostly javascript compatible as well.
By default, you can assign null and undefeined to any variable. This can lead to errors.
The strictNullChecks flag (in .config file) fixes this:
let exampleString = "foo";
exampleString = null;
// Type 'null' is not assignable to type 'string'.
let stringOrNull: string | null = "bar";
stringOrNull = null;
stringOrNull = undefined;
Type 'undefined' is not assignable to type 'string | null'.
type Container<T> = { value: T };
We can also have a type alias refer to itself in a property:
type Tree<T> = {
value: T;
left?: Tree<T>; // Note: Recursive reference to Type!
right?: Tree<T>;
};
type LinkedList<Type> = Type & { next: LinkedList<Type> };
Generic functions are mainly used for type inference of return values:
function reverse<T>(items: T[]): T[] {
var toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
var sample = [1, 2, 3];
var reversed = reverse(sample);
console.log(reversed); // 3,2,1
// Safety! cols
reversed[0] = '1'; // Error!
reversed = ['1', '2']; // Error!
reversed[0] = 1; // Okay
// Note: Array has built-in reverse defined. This is just to illustrate.
// Static code analysis detects the function arg type and derives return type.
Generic class:
/** A class definition with a generic parameter */
class Queue<T> {
private data = [];
push(item: T) { this.data.push(item); }
pop(): T | undefined { return this.data.shift(); }
}
/** Again sample usage */
const queue = new Queue<number>();
queue.push(0);
queue.push("1"); // ERROR : cannot push a string. Only numbers allowed
const MyComponent: React.FunctionComponent<Props> = (props) => {
return <span>{props.foo}</span>
}
<MyComponent foo="bar" />
class MyAwesomeComponent extends React.Component {
render() {
return <div>Hello</div>;
}
}
const foo: React.ReactElement<MyAwesomeComponent> = <MyAwesomeComponent />; //Ok
const bar: React.ReactElement<MyAwesomeComponent> = <NotMyAwesomeComponent />; // Error!
// Compiled with --strictNullChecks
function validateEntity(e?: Entity) {
// Throw exception if e is null or invalid entity
}
function processEntity(e?: Entity) {
validateEntity(e); // If it passes this line, e is not null!
let a = e.name; // TS ERROR: e may be null.
let b = e!.name; // OKAY. We are asserting that e is non-null.
}
var foo = 123;
foo = '456'; // Error: cannot assign a `string` to a `number`