Classes in TypeScript

You might also like

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 5

While TypeScript is very simple to understand when performing basic tasks, having

a deeper understanding of how its type system works is critical to unlocking


advanced language functionality. Once we know more about how TypeScript really
works, we can leverage this knowledge to write cleaner, well-organized code.

Behind the class keyword
In TypeScript, the class keyword provides a more familiar syntax for generating
constructor functions and performing simple inheritance. It has roughly the same
syntax as the ES2015 class syntax, but with a few key distinctions. Most notably, it
allows for non-method properties, similar to this Stage 3 proposal. In fact, declaration
of each instance method or property that will be used by the class is mandatory, as
this will be used to build up a type for the value of this within the class.

But what if we couldn’t use the class keyword for some reason? How would we
make an equivalent structure? Is it even possible? To answer these questions, let’s
start with a basic example of a TypeScript class:
class Point {
  static fromOtherPoint(point: Point): Point {
    // ...
  }
   x: number;
  y: number;
   constructor(x: number, y: number) {
    // ...
  }
   toString(): string {
    // ...
  }
}

This archetypical class includes a static method, instance properties, and instance
methods. When creating a new instance of this type, we’d call new Point(<number>,
<number>), and when referring to an instance of this type, we’d use the type Point.
But how does this work? Aren’t the Point type and the Point constructor the same
thing? Actually, no!

In TypeScript, types are overlaid onto JavaScript code through an entirely separate
type system, rather than becoming part of the JavaScript code itself. This means
that an interface (“type”) in TypeScript can—and often does—use the same
identifier name as a variable in JavaScript without introducing a name conflict.
(The only time that an identifier in the type system refers to a name within
JavaScript is when the typeof operator is used.)

When using the class keyword in TypeScript, you are actually creating two things


with the same identifier:
 A TypeScript interface containing all the instance methods and properties of
the class; and
 A JavaScript variable with a different (anonymous) constructor function type

In other words, the example class above is effectively just shorthand for this code:
// our TypeScript `Point` type
interface Point {
  x: number;
  y: number;
  toString(): string;
}
// our JavaScript `Point` variable, with a constructor type
let Point: {
  new (x: number, y: number): Point;
  prototype: Point;
 
  // static class properties and methods are actually part
  // of the constructor type!
  fromOtherPoint(point: Point): Point;
};
 
// `Function` does not fulfill the defined type so
// it needs to be cast to <any>
Point = <any> function (this: Point, x: number, y: number): void {
  // ...
};
 
// static properties/methods go on the JavaScript variable...
Point.fromOtherPoint = function (point: Point): Point {
  // ...
};
 // instance properties/methods go on the prototype
Point.prototype.toString = function (): string {
  // ...
};

TypeScript also has support for ES6 Class expressions.

Adding type properties to classes


As mentioned above, adding non-method properties to classes in TypeScript is
encouraged and required for the type system to understand what is available on
the class.
class Widget {
  className: string;
  color: string = 'red';
  id: string;
}
In this example, className, color, and id have been defined as being properties
that can exist on the class. However by default, className and id have no value.
TypeScript can warn us about this with the --
strictPropertyInitialization flag, which will throw an error if a class property
is not assigned a value directly on the definition, or within the constructor. The
value assigned to color is not actually assigned directly to the prototype.
Instead, it’s value is assigned inside the constructor in the transpiled code,
meaning that it is safe to assign non-primitive types directly without any risk of
accidentally sharing those values with all instances of the class.

A common problem in complex applications is how to keep related sets of


functionality grouped together. We already accomplish this by doing things like
organising code into modules for large sets of functionality, but what about things
like types that are only applicable to a single class or interface? For example, what
if we had a Widget class that accepted a keyword arguments object:

export class Widget {


  constructor(kwArgs: {
    className?: string;
    id?: string;
    style?: Object;
  }) {
    // ...
  }
}
 export default Widget;
In this code, we’ve succeeded in defining an anonymous type for
the kwArgs parameter, but this is very brittle. What happens when we
subclass Widget and want to add some extra properties? We’d have to write the
entire type all over again. Or, what if we want to reference this type in multiple
places, like within some code that instantiates a Widget? We wouldn’t be able to,
because it’s an anonymous type assigned to a function parameter.

To solve this problem, we can use an interface to define the constructor


arguments and export that alongside the class.
export interface WidgetProperties {
  className?: string;
  id?: string;
  style?: Object | Style
}
 export class Widget {
  constructor(kwArgs: WidgetProperties = {}) {
    for (let key in kwArgs) {
      this[key] = kwArgs[key];
    }
  }
}
 export default Widget;

Now, instead of having an anonymous object type dirtying up our code, we have a
specific WidgetProperties interface that can be referenced by our code as well
as any other code that imports Widget. This means that we can easily subclass
our kwArgs parameter while keeping everything DRY and well-organized:

import Widget, { WidgetProperties } from './Widget';


 
export interface TextInputProperties extends WidgetProperties {
    maxLength?: number;
    placeholder?: string;
    value?: string;
}
 // normal class inheritance…
export class TextInput extends Widget {
  // replace the parameter type with our new, more specific subtype
  constructor(kwArgs: TextInputProperties = {}) {
    super(kwArgs);
  }
}
 export default TextInput;

As mentioned earlier, using this pattern, we can also reference these types from
other code by importing the interfaces where they are needed:
import Widget, { WidgetProperties } from './Widget';
import TextInput from './TextInput';
 export function createWidget<
  T extends Widget = Widget,
  K extends WidgetProperties = WidgetProperties
>(Ctor: { new (...args: any[]): T; }, kwArgs: K): T {
  return new Ctor(kwArgs);
}
 // w has type `Widget`
const w = createWidget(Widget, { style: { backroundColor: 'red' } });
// t has type `TextInput`
const t = createWidget(TextInput, { style: { backgroundColor: 'green' }
});

Access Modifiers
Another welcome addition to classes in TypeScript are access modifiers that allow
the developer to declare methods and properties as public, private, protected,
and readonly.

class Widget {
  class: string; // No modifier implies public
  private _id: string;
  readonly id: string; /
 
  protected foo() {
    // ...
  }
}

If no modifier is provided, then the method or property is assumed to


be public which means it can be accessed internally or externally. If it is marked
as private then the method or property is only accessible internally within the
class. These modifier is only enforceable at compile-time, however. The
TypeScript compiler will warn about all inappropriate uses, but it does nothing to
stop inappropriate usage at runtime. protected implies that the method or
property is accessible only internally within the class or any class that extends it
but not externally. Finally, readonly will cause the TypeScript compiler to throw an
error if the value of the property is changed after its its initial assignment in the
class constructor.

Decorators

Please note that decorators were added to TypeScript early and are only available
with the –experimentalDecorators flag because they do not reflect the
current state of the TC39 proposal. A decorator is a function that allows shorthand
in-line modification of classes, properties, methods, and parameters. A method
decorator receives 3 parameters:

 target: the object the method is defined on


 key: the name of the method
 descriptor: the object descriptor for the method

The decorator function can optionally return a property descriptor to install on the
target object.
function myDecorator(target, key, descriptor) {
}
 class MyClass{
    @myDecorator
    myMethod() {}
}

myDecorator would be invoked with the parameter


values MyClass.prototype, 'myMethod',
and Object.getOwnPropertyDescriptor(MyClass.prototype, 'myMethod').

TypeScript also supports computed property names and Unicode escape sequences.

You might also like