Skip to content

Instantly share code, notes, and snippets.

@jarhoads
Created June 4, 2019 17:14
Show Gist options
  • Save jarhoads/232c52e0439f965493cf6455f3731dfd to your computer and use it in GitHub Desktop.
Save jarhoads/232c52e0439f965493cf6455f3731dfd to your computer and use it in GitHub Desktop.
typescript classes
// constructors
// All classes in TypeScript have a constructor, whether you specify one or not.
// If you leave out the constructor, the compiler will automatically add one.
// For a class that doesn't inherit from another class, the automatic constructor will be
// parameterless and will initialize any class properties.
// Where the class extends another class, the automatic constructor will
// match the superclass signature and will pass arguments to the superclass before
// initializing any of its own properties.
class Song {
constructor(private artist: string, private title: string) { }
play() {
console.log('Playing ' + this.title + ' by ' + this.artist);
}
}
class Jukebox {
constructor(private songs: Song[]) { }
play() {
const song = this.getRandomSong();
song.play();
}
private getRandomSong() {
const songCount = this.songs.length;
const songIndex = Math.floor(Math.random() * songCount);
return this.songs[songIndex];
}
}
const songs = [
new Song('Bushbaby', 'Megaphone'),
new Song('Delays', 'One More Lie In'),
new Song('Goober Gun', 'Stereo'),
new Song('Sohnee', 'Shatter'),
new Song('Get Amped', 'Celebrity')
];
const jukebox = new Jukebox(songs);
jukebox.play();
// You can also add static properties to your class, which are defined in the same way
// as instance properties, but with the static keyword between the access modifier
// (if one is specified) and the identifier.
// You can make both static and instance properties read-only to prevent the values being overwritten.
class Playlist {
private songs: Song[] = [];
static readonly maxSongCount = 30;
constructor(public name: string) {
}
addSong(song: Song) {
if (this.songs.length >= Playlist.maxSongCount) {
throw new Error('Playlist is full');
}
this.songs.push(song);
}
}
// Creating a new instance
const playlist = new Playlist('My Playlist');
// Accessing a public instance property
const playName = playlist.name;
// Calling a public instance method
playlist.addSong(new Song('Therapy?', 'Crooked Timber'));
// Accessing a public static property
const maxSongs = Playlist.maxSongCount;
// Error: Cannot assign to a readonly property
// Playlist.maxSongCount = 20;
// TypeScript supports property getters and setters, if you are targeting ECMAScript 5 or above
// property getters and setters allow you to wrap property access with a method while
// preserving the appearance of a simple property to the calling code.
interface StockItem {
description: string;
asin: string;
}
class WarehouseLocation {
private _stockItem: StockItem;
constructor(public aisle: number, public slot: string) { }
get stockItem() {
return this._stockItem;
}
set stockItem(item: StockItem) {
this._stockItem = item;
}
}
const figure = { asin: 'B001TEQ2PI', description: 'Figure' };
const warehouseSlot = new WarehouseLocation(15, 'A6');
warehouseSlot.stockItem = figure;
// There are two types of class heritage in TypeScript.
// A class can implement an interface using the implements keyword and
// a class can inherit from another class using the extends keyword.
// If you do specify the interface using the implements keyword,
// your class will be checked to ensure that it complies with the contract promised by the interface.
interface Audio {
play(): any;
}
class AudioSong implements Audio {
constructor(private artist: string, private title: string) { }
play(): void {
console.log('Playing ' + this.title + ' by ' + this.artist);
}
static Comparer(a: AudioSong, b: AudioSong) {
if (a.title === b.title) {
return 0;
}
return a.title > b.title ? 1 : -1;
}
}
class AudioPlaylist {
constructor(public songs: Audio[]) {
}
play() {
var song = this.songs.pop();
song.play();
}
sort() {
this.songs.sort(AudioSong.Comparer);
}
}
//An extends clause makes your class a derived class,
// and it will gain all the properties and methods of the base class from which it inherits.
// You can override a public member of the base class by adding a member of the same name and kind as the base class member.
// The RepeatingPlaylist inherits from the Playlist class and uses the songs property from the base class using this.songs,
// but overrides the play method with a specialized implementation that plays the next song in a repeating loop.
class RepeatingPlaylist extends AudioPlaylist {
private songIndex = 0;
// the constructor could be omitted because the automatic constructor
// that would be generated would match it exactly.
constructor(songs: AudioSong[]) {
super(songs);
}
play() {
this.songs[this.songIndex].play;
this.songIndex++;
if (this.songIndex >= this.songs.length) {
this.songIndex = 0;
}
}
}
// If the subclass accepts additional arguments there are a couple of rules you need to follow.
// The super call to the base class must be the first statement in the subclass constructor and
// you cannot specify a more restrictive access modifier for a parameter on the subclass than it has on the base class.
// There are some rules that must be followed for inheritance:
// A class can only inherit from a single superclass.
// A class cannot inherit from itself, either directly or via a chain of inheritance.
// It is possible to create a class that inherits from another class and implements multiple interfaces.
// Abstract Class
// An abstract class can be used as a base class, but can't be instantiated directly.
// Abstract classes can contain implemented methods as well as abstract methods,
// which have no implementation and must be implemented by any subclass.
// abstract logger class with an abstract notify method, and an implemented protected getMessage method.
// Each subclass must implement the notify method, but can share the getMessage implementation from the base class.
// Abstract class
abstract class Logger {
abstract notify(message: string): void;
protected getMessage(message: string): string {
return `Information: ${new Date().toUTCString()} ${message}`;
}
}
class ConsoleLogger extends Logger {
notify(message) {
console.log(this.getMessage(message));
}
}
class InvasiveLogger extends Logger {
notify(message) {
alert(this.getMessage(message));
}
}
let logger: Logger;
// Error. Cannot create an instance of an abstract class
// logger = new Logger();
// Create an instance of a sub-class
logger = new InvasiveLogger();
logger.notify('Hello World');
// Abstract classes are like interfaces, in that they contain a contract that may have no implementation.
// They can add to this with implementation code, and can specify access modifiers against the members:
// two things that interfaces cannot do.
// Scope
// If you call a class method from an event, or use it as a callback,
// the original context of the method can be lost,
// which results in problems using instance methods and instance properties.
// When the context is changed, the value of the this keyword is replaced.
// typical example of lost context.
// If the registerClick method is called directly against the clickCounter instance,
// it works as expected.
// When the registerClick method is assigned to the onclick event,
// the context is lost and this.count is undefined in the new context.
class ClickCounter {
private count = 0;
registerClick() {
this.count++;
alert(this.count);
}
}
const clickCounter = new ClickCounter();
document.getElementById('target').onclick = clickCounter.registerClick;
// several techniques that can be used to preserve the context to enable this to work
// Proprty and Arrow Function
// You can replace the method with a property and initialize the property using an arrow function
// This is a reasonable technique if you know that the class will be consumed with events or callbacks,
// but it is less of an option if your class has no knowledge of when and where it may be called.
class ClickCounterArrow {
private count = 0;
registerClick = () => {
this.count++;
alert(this.count);
}
}
// Function Wrapping at Point of Call
// If you want to leave your class untouched, you can wrap the call to the instance method in a function
// to create a closure that keeps the context alongside the function.
document.getElementById('target').onclick = function () {
clickCounter.registerClick();
};
// ECMAScript 5 Bind Function
// Another technique that leaves the original class untouched is to use JavaScript's bind function,
// which is available in ECMAScript 5 and higher.
// The bind function sets the context for the method.
// It can be used more generally to permanently replace the context, but here it is used to fix the context
// for the registerClick method to be the clickCounter instance.
const clickHandler = clickCounter.registerClick.bind(clickCounter);
document.getElementById('target').onclick = clickHandler;
// Event Capturing
// If you need to capture the event argument, the simplest way is to use an arrow function
// The registerClick method has been updated to take an identifier,
// which is obtained using the event target (or source element in older versions of Internet Explorer).
// This preserves the context and captures the event information in one terse statement.
class ClickCounterID {
private count = 0;
registerClick(id: string) {
this.count++;
alert(this.count);
}
}
const clickCounterID = new ClickCounterID();
document.getElementById('target').onclick = (e) => {
const target = <Element>e.target || e.srcElement;
clickCounterID.registerClick(target.id);
};
// One thing to consider when designing your program will be the number of instances of each class being created at runtime.
// If you are creating hundreds or thousands of instances it is more efficient for the methods to be normal instance methods,
// not arrow functions assigned to properties.
// This is because normal instance methods are defined once and used by all instances.
// If you use a property and arrow function, it will be duplicated on every instance.
// This duplication can become a big overhead when a large number of instances are created.
// If you want to keep a clear divide between responsibilities, follow this guideline;
// when you need to preserve the scope of a callback,
// prefer to preserve it when setting up the callback, not by adjusting the class itself.
// Type Information
// Checking types and branching off in different directions based on the type
// is a strong indicator that you have broken encapsulation.
// To test the type of a class instance, you use the instanceof operator.
class Display {
name: string = '';
}
class Television extends Display { }
class HiFi { }
const display = new Display();
const television = new Television();
const hiFi = new HiFi();
let isDisplay;
// true
isDisplay = display instanceof Display;
// true (inherits from Display)
isDisplay = television instanceof Display;
// false
isDisplay = hiFi instanceof Display;
// You can also test the presence of specific properties using the in keyword.
let hasName;
// true
hasName = 'name' in display;
// true
hasName = 'name' in television;
// false
hasName = 'name' in hiFi;
// It is important to note that due to the code generation in the TypeScript compiler,
// an uninitialized property will not be detected because unless the property has a value,
// it does not appear in the compiled JavaScript code.
// the hasName property will be false because,
// although a name property is declared, the name property is never initialized.
// If the name property had been assigned a value, hasName would be true.
// Don't forget the quotes around the property name when using the in keyword as you will need to pass a string.
// Without the quotes, you would be testing the value of a variable, which may not even be defined.
class DisplayName {
name: string;
}
const displayName = new DisplayName();
// false
const hasNameDisplay = 'name' in displayName;
// If you want to obtain the type name at runtime, you may be tempted to use the typeof operator.
// Unfortunately, this will return the type name 'object' for all classes.
// This means you need to inspect the constructor of the instance to find the type name.
const tv = new Television();
const radio = new HiFi();
// Television
const tvType = tv.constructor.name;
// HiFi
const radioType = radio.constructor.name;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment