Classes
Background Reading:
Classes (MDN)
TypeScript offers full support for theclass keyword introduced in ES2015.
As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you to express relationships between classes and other types.
Class Members
Here’s the most basic class - an empty one:
tsTryclassPoint {}
This class isn’t very useful yet, so let’s start adding some members.
Fields
A field declaration creates a public writeable property on a class:
tsTryclassPoint {x :number;y :number;}constpt =newPoint ();pt .x =0;pt .y =0;
As with other locations, the type annotation is optional, but will be an implicitany if not specified.
Fields can also haveinitializers; these will run automatically when the class is instantiated:
tsTryclassPoint {x =0;y =0;}constpt =newPoint ();// Prints 0, 0console .log (`${pt .x },${pt .y }`);
Just like withconst,let, andvar, the initializer of a class property will be used to infer its type:
tsTryconstpt =newPoint ();Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.pt .x ="0";
--strictPropertyInitialization
ThestrictPropertyInitialization setting controls whether class fields need to be initialized in the constructor.
tsTryclassBadGreeter {Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.:string; name }
tsTryclassGoodGreeter {name :string;constructor() {this.name ="hello";}}
Note that the field needs to be initializedin the constructor itself.TypeScript does not analyze methods you invoke from the constructor to detect initializations, because a derived class might override those methods and fail to initialize the members.
If you intend to definitely initialize a field through means other than the constructor (for example, maybe an external library is filling in part of your class for you), you can use thedefinite assignment assertion operator,!:
tsTryclassOKGreeter {// Not initialized, but no errorname !:string;}
readonly
Fields may be prefixed with thereadonly modifier.This prevents assignments to the field outside of the constructor.
tsTryclassGreeter {readonlyname :string ="world";constructor(otherName ?:string) {if (otherName !==undefined ) {this.name =otherName ;}}err () {this.Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.="not ok"; name }}constg =newGreeter ();Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.g .="also not ok"; name
Constructors
Background Reading:
Constructor (MDN)
Class constructors are very similar to functions.You can add parameters with type annotations, default values, and overloads:
tsTryclassPoint {x :number;y :number;// Normal signature with defaultsconstructor(x =0,y =0) {this.x =x ;this.y =y ;}}
tsTryclassPoint {x :number =0;y :number =0;// Constructor overloadsconstructor(x :number,y :number);constructor(xy :string);constructor(x :string |number,y :number =0) {// Code logic here}}
There are just a few differences between class constructor signatures and function signatures:
- Constructors can’t have type parameters - these belong on the outer class declaration, which we’ll learn about later
- Constructors can’t have return type annotations - the class instance type is always what’s returned
Super Calls
Just as in JavaScript, if you have a base class, you’ll need to callsuper(); in your constructor body before using anythis. members:
tsTryclassBase {k =4;}classDerived extendsBase {constructor() {// Prints a wrong value in ES5; throws exception in ES6'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.console .log (this .k );super();}}
Forgetting to callsuper is an easy mistake to make in JavaScript, but TypeScript will tell you when it’s necessary.
Methods
Background Reading:
Method definitions
A function property on a class is called amethod.Methods can use all the same type annotations as functions and constructors:
tsTryclassPoint {x =10;y =10;scale (n :number):void {this.x *=n ;this.y *=n ;}}
Other than the standard type annotations, TypeScript doesn’t add anything else new to methods.
Note that inside a method body, it is still mandatory to access fields and other methods viathis..An unqualified name in a method body will always refer to something in the enclosing scope:
tsTryletx :number =0;classC {x :string ="hello";m () {// This is trying to modify 'x' from line 1, not the class propertyType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.="world"; x }}
Getters / Setters
Classes can also haveaccessors:
tsTryclassC {_length =0;getlength () {returnthis._length ;}setlength (value ) {this._length =value ;}}
Note that a field-backed get/set pair with no extra logic is very rarely useful in JavaScript.It’s fine to expose public fields if you don’t need to add additional logic during the get/set operations.
TypeScript has some special inference rules for accessors:
- If
getexists but noset, the property is automaticallyreadonly - If the type of the setter parameter is not specified, it is inferred from the return type of the getter
SinceTypeScript 4.3, it is possible to have accessors with different types for getting and setting.
tsTryclassThing {_size =0;getsize ():number {returnthis._size ;}setsize (value :string |number |boolean) {letnum =Number (value );// Don't allow NaN, Infinity, etcif (!Number .isFinite (num )) {this._size =0;return;}this._size =num ;}}
Index Signatures
Classes can declare index signatures; these work the same asIndex Signatures for other object types:
tsTryclassMyClass {[s :string]:boolean | ((s :string)=>boolean);check (s :string) {returnthis[s ]asboolean;}}
Because the index signature type needs to also capture the types of methods, it’s not easy to usefully use these types.Generally it’s better to store indexed data in another place instead of on the class instance itself.
Class Heritage
Like other languages with object-oriented features, classes in JavaScript can inherit from base classes.
implements Clauses
You can use animplements clause to check that a class satisfies a particularinterface.An error will be issued if a class fails to correctly implement it:
tsTryinterfacePingable {ping ():void;}classSonar implementsPingable {ping () {console .log ("ping!");}}classClass 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.2420Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.implements Ball Pingable {pong () {console .log ("pong!");}}
Classes may also implement multiple interfaces, e.g.class C implements A, B {.
Cautions
It’s important to understand that animplements clause is only a check that the class can be treated as the interface type.It doesn’t change the type of the class or its methodsat all.A common source of error is to assume that animplements clause will change the class type - it doesn’t!
tsTryinterfaceCheckable {check (name :string):boolean;}classNameChecker implementsCheckable {Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.check () { s // Notice no error herereturns .toLowerCase () ==="ok";}}
In this example, we perhaps expected thats’s type would be influenced by thename: string parameter ofcheck.It is not -implements clauses don’t change how the class body is checked or its type inferred.
Similarly, implementing an interface with an optional property doesn’t create that property:
tsTryinterfaceA {x :number;y ?:number;}classC implementsA {x =0;}constc =newC ();Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.c .=10; y
extends Clauses
Background Reading:
extends keyword (MDN)
Classes mayextend from a base class.A derived class has all the properties and methods of its base class, and can also define additional members.
tsTryclassAnimal {move () {console .log ("Moving along!");}}classDog extendsAnimal {woof (times :number) {for (leti =0;i <times ;i ++) {console .log ("woof!");}}}constd =newDog ();// Base class methodd .move ();// Derived class methodd .woof (3);
Overriding Methods
Background Reading:
super keyword (MDN)
A derived class can also override a base class field or property.You can use thesuper. syntax to access base class methods.Note that because JavaScript classes are a simple lookup object, there is no notion of a “super field”.
TypeScript enforces that a derived class is always a subtype of its base class.
For example, here’s a legal way to override a method:
tsTryclassBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {greet (name ?:string) {if (name ===undefined ) {super.greet ();}else {console .log (`Hello,${name .toUpperCase ()}`);}}}constd =newDerived ();d .greet ();d .greet ("reader");
It’s important that a derived class follow its base class contract.Remember that it’s very common (and always legal!) to refer to a derived class instance through a base class reference:
tsTry// Alias the derived instance through a base class referenceconstb :Base =d ;// No problemb .greet ();
What ifDerived didn’t followBase’s contract?
tsTryclassBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {// Make this parameter requiredProperty 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.2416Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.( greet name :string) {console .log (`Hello,${name .toUpperCase ()}`);}}
If we compiled this code despite the error, this sample would then crash:
tsTryconstb :Base =newDerived ();// Crashes because "name" will be undefinedb .greet ();
Type-only Field Declarations
Whentarget >= ES2022 oruseDefineForClassFields istrue, class fields are initialized after the parent class constructor completes, overwriting any value set by the parent class. This can be a problem when you only want to re-declare a more accurate type for an inherited field. To handle these cases, you can writedeclare to indicate to TypeScript that there should be no runtime effect for this field declaration.
tsTryinterfaceAnimal {dateOfBirth :any;}interfaceDog extendsAnimal {breed :any;}classAnimalHouse {resident :Animal ;constructor(animal :Animal ) {this.resident =animal ;}}classDogHouse extendsAnimalHouse {// Does not emit JavaScript code,// only ensures the types are correctdeclareresident :Dog ;constructor(dog :Dog ) {super(dog );}}
Initialization Order
The order that JavaScript classes initialize can be surprising in some cases.Let’s consider this code:
tsTryclassBase {name ="base";constructor() {console .log ("My name is " +this.name );}}classDerived extendsBase {name ="derived";}// Prints "base", not "derived"constd =newDerived ();
What happened here?
The order of class initialization, as defined by JavaScript, is:
- The base class fields are initialized
- The base class constructor runs
- The derived class fields are initialized
- The derived class constructor runs
This means that the base class constructor saw its own value forname during its own constructor, because the derived class field initializations hadn’t run yet.
Inheriting Built-in Types
Note: If you don’t plan to inherit from built-in types like
Array,Error,Map, etc. or your compilation target is explicitly set toES6/ES2015or above, you may skip this section
In ES2015, constructors which return an object implicitly substitute the value ofthis for any callers ofsuper(...).It is necessary for generated constructor code to capture any potential return value ofsuper(...) and replace it withthis.
As a result, subclassingError,Array, and others may no longer work as expected.This is due to the fact that constructor functions forError,Array, and the like use ECMAScript 6’snew.target to adjust the prototype chain;however, there is no way to ensure a value fornew.target when invoking a constructor in ECMAScript 5.Other downlevel compilers generally have the same limitation by default.
For a subclass like the following:
tsTryclassMsgError extendsError {constructor(m :string) {super(m );}sayHello () {return"hello " +this.message ;}}
you may find that:
- methods may be
undefinedon objects returned by constructing these subclasses, so callingsayHellowill result in an error. instanceofwill be broken between instances of the subclass and their instances, so(new MsgError()) instanceof MsgErrorwill returnfalse.
As a recommendation, you can manually adjust the prototype immediately after anysuper(...) calls.
tsTryclassMsgError extendsError {constructor(m :string) {super(m );// Set the prototype explicitly.Object .setPrototypeOf (this,MsgError .prototype );}sayHello () {return"hello " +this.message ;}}
However, any subclass ofMsgError will have to manually set the prototype as well.For runtimes that don’t supportObject.setPrototypeOf, you may instead be able to use__proto__.
Unfortunately,these workarounds will not work on Internet Explorer 10 and prior.One can manually copy methods from the prototype onto the instance itself (i.e.MsgError.prototype ontothis), but the prototype chain itself cannot be fixed.
Member Visibility
You can use TypeScript to control whether certain methods or properties are visible to code outside the class.
public
The default visibility of class members ispublic.Apublic member can be accessed anywhere:
tsTryclassGreeter {publicgreet () {console .log ("hi!");}}constg =newGreeter ();g .greet ();
Becausepublic is already the default visibility modifier, you don’t everneed to write it on a class member, but might choose to do so for style/readability reasons.
protected
protected members are only visible to subclasses of the class they’re declared in.
tsTryclassGreeter {publicgreet () {console .log ("Hello, " +this.getName ());}protectedgetName () {return"hi";}}classSpecialGreeter extendsGreeter {publichowdy () {// OK to access protected member hereconsole .log ("Howdy, " +this.getName ());}}constg =newSpecialGreeter ();g .greet ();// OKProperty 'getName' is protected and only accessible within class 'Greeter' and its subclasses.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.g .(); getName
Exposure ofprotected members
Derived classes need to follow their base class contracts, but may choose to expose a subtype of base class with more capabilities.This includes makingprotected memberspublic:
tsTryclassBase {protectedm =10;}classDerived extendsBase {// No modifier, so default is 'public'm =15;}constd =newDerived ();console .log (d .m );// OK
Note thatDerived was already able to freely read and writem, so this doesn’t meaningfully alter the “security” of this situation.The main thing to note here is that in the derived class, we need to be careful to repeat theprotected modifier if this exposure isn’t intentional.
Cross-hierarchyprotected access
TypeScript doesn’t allow accessingprotected members of a sibling class in a class hierarchy:
tsTryclassBase {protectedx :number =1;}classDerived1 extendsBase {protectedx :number =5;}classDerived2 extendsBase {f1 (other :Derived2 ) {other .x =10;}f2 (other :Derived1 ) {Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.2445Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.other .=10; x }}
This is because accessingx inDerived2 should only be legal fromDerived2’s subclasses, andDerived1 isn’t one of them.Moreover, if accessingx through aDerived1 reference is illegal (which it certainly should be!), then accessing it through a base class reference should never improve the situation.
See alsoWhy Can’t I Access A Protected Member From A Derived Class? which explains more of C#‘s reasoning on the same topic.
private
private is likeprotected, but doesn’t allow access to the member even from subclasses:
tsTryclassBase {privatex =0;}constb =newBase ();// Can't access from outside the classProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (b .); x
tsTryclassDerived extendsBase {showX () {// Can't access in subclassesProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (this.); x }}
Becauseprivate members aren’t visible to derived classes, a derived class can’t increase their visibility:
tsTryclassBase {privatex =0;}classClass 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.extends Derived Base {x =1;}
Cross-instanceprivate access
Different OOP languages disagree about whether different instances of the same class may access each others’private members.While languages like Java, C#, C++, Swift, and PHP allow this, Ruby does not.
TypeScript does allow cross-instanceprivate access:
tsTryclassA {privatex =10;publicsameAs (other :A ) {// No errorreturnother .x ===this.x ;}}
Caveats
Like other aspects of TypeScript’s type system,private andprotectedare only enforced during type checking.
This means that JavaScript runtime constructs likein or simple property lookup can still access aprivate orprotected member:
tsTryclassMySafe {privatesecretKey =12345;}
js// In a JavaScript file...consts =newMySafe();// Will print 12345console.log(s.secretKey);
private also allows access using bracket notation during type checking. This makesprivate-declared fields potentially easier to access for things like unit tests, with the drawback that these fields aresoft private and don’t strictly enforce privacy.
tsTryclassMySafe {privatesecretKey =12345;}consts =newMySafe ();// Not allowed during type checkingProperty 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.console .log (s .); secretKey // OKconsole .log (s ["secretKey"]);
Unlike TypeScripts’sprivate, JavaScript’sprivate fields (#) remain private after compilation and do not provide the previously mentioned escape hatches like bracket notation access, making themhard private.
tsTryclassDog {#barkAmount =0;personality ="happy";constructor() {}}
tsTry"use strict";classDog {#barkAmount =0;personality ="happy";constructor() { }}
When compiling to ES2021 or less, TypeScript will use WeakMaps in place of#.
tsTry"use strict";var_Dog_barkAmount;classDog {constructor() {_Dog_barkAmount.set(this,0);this.personality ="happy";}}_Dog_barkAmount =newWeakMap();
If you need to protect values in your class from malicious actors, you should use mechanisms that offer hard runtime privacy, such as closures, WeakMaps, or private fields. Note that these added privacy checks during runtime could affect performance.
Static Members
Background Reading:
Static Members (MDN)
Classes may havestatic members.These members aren’t associated with a particular instance of the class.They can be accessed through the class constructor object itself:
tsTryclassMyClass {staticx =0;staticprintX () {console .log (MyClass .x );}}console .log (MyClass .x );MyClass .printX ();
Static members can also use the samepublic,protected, andprivate visibility modifiers:
tsTryclassMyClass {privatestaticx =0;}Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.console .log (MyClass .); x
Static members are also inherited:
tsTryclassBase {staticgetGreeting () {return"Hello world";}}classDerived extendsBase {myGreeting =Derived .getGreeting ();}
Special Static Names
It’s generally not safe/possible to overwrite properties from theFunction prototype.Because classes are themselves functions that can be invoked withnew, certainstatic names can’t be used.Function properties likename,length, andcall aren’t valid to define asstatic members:
tsTryclassS {staticStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.="S!"; name }
Why No Static Classes?
TypeScript (and JavaScript) don’t have a construct calledstatic class the same way as, for example, C# does.
Those constructsonly exist because those languages force all data and functions to be inside a class; because that restriction doesn’t exist in TypeScript, there’s no need for them.A class with only a single instance is typically just represented as a normalobject in JavaScript/TypeScript.
For example, we don’t need a “static class” syntax in TypeScript because a regular object (or even top-level function) will do the job just as well:
tsTry// Unnecessary "static" classclassMyStaticClass {staticdoSomething () {}}// Preferred (alternative 1)functiondoSomething () {}// Preferred (alternative 2)constMyHelperObject = {dosomething () {},};
static Blocks in Classes
Static blocks allow you to write a sequence of statements with their own scope that can access private fields within the containing class. This means that we can write initialization code with all the capabilities of writing statements, no leakage of variables, and full access to our class’s internals.
tsTryclassFoo {static#count =0;getcount () {returnFoo .#count;}static {try {constlastInstances =loadLastInstances ();Foo .#count +=lastInstances .length ;}catch {}}}
Generic Classes
Classes, much like interfaces, can be generic.When a generic class is instantiated withnew, its type parameters are inferred the same way as in a function call:
tsTryclassBox <Type > {contents :Type ;constructor(value :Type ) {this.contents =value ;}}constb =newBox ("hello!");
Classes can use generic constraints and defaults the same way as interfaces.
Type Parameters in Static Members
This code isn’t legal, and it may not be obvious why:
tsTryclassBox <Type > {staticStatic members cannot reference class type parameters.2302Static members cannot reference class type parameters.defaultValue :; Type }
Remember that types are always fully erased!At runtime, there’s onlyoneBox.defaultValue property slot.This means that settingBox<string>.defaultValue (if that were possible) wouldalso changeBox<number>.defaultValue - not good.Thestatic members of a generic class can never refer to the class’s type parameters.
this at Runtime in Classes
Background Reading:
this keyword (MDN)
It’s important to remember that TypeScript doesn’t change the runtime behavior of JavaScript, and that JavaScript is somewhat famous for having some peculiar runtime behaviors.
JavaScript’s handling ofthis is indeed unusual:
tsTryclassMyClass {name ="MyClass";getName () {returnthis.name ;}}constc =newMyClass ();constobj = {name :"obj",getName :c .getName ,};// Prints "obj", not "MyClass"console .log (obj .getName ());
Long story short, by default, the value ofthis inside a function depends onhow the function was called.In this example, because the function was called through theobj reference, its value ofthis wasobj rather than the class instance.
This is rarely what you want to happen!TypeScript provides some ways to mitigate or prevent this kind of error.
Arrow Functions
Background Reading:
Arrow functions (MDN)
If you have a function that will often be called in a way that loses itsthis context, it can make sense to use an arrow function property instead of a method definition:
tsTryclassMyClass {name ="MyClass";getName = ()=> {returnthis.name ;};}constc =newMyClass ();constg =c .getName ;// Prints "MyClass" instead of crashingconsole .log (g ());
This has some trade-offs:
- The
thisvalue is guaranteed to be correct at runtime, even for code not checked with TypeScript - This will use more memory, because each class instance will have its own copy of each function defined this way
- You can’t use
super.getNamein a derived class, because there’s no entry in the prototype chain to fetch the base class method from
this parameters
In a method or function definition, an initial parameter namedthis has special meaning in TypeScript.These parameters are erased during compilation:
tsTry// TypeScript input with 'this' parameterfunctionfn (this :SomeType ,x :number) {/* ... */}
js// JavaScript outputfunctionfn(x) {/* ... */}
TypeScript checks that calling a function with athis parameter is done so with a correct context.Instead of using an arrow function, we can add athis parameter to method definitions to statically enforce that the method is called correctly:
tsTryclassMyClass {name ="MyClass";getName (this :MyClass ) {returnthis.name ;}}constc =newMyClass ();// OKc .getName ();// Error, would crashconstg =c .getName ;The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.console .log (g ());
This method makes the opposite trade-offs of the arrow function approach:
- JavaScript callers might still use the class method incorrectly without realizing it
- Only one function per class definition gets allocated, rather than one per class instance
- Base method definitions can still be called via
super.
this Types
In classes, a special type calledthis refersdynamically to the type of the current class.Let’s see how this is useful:
tsTryclassBox {contents :string ="";set (value :string) {this.contents =value ;returnthis;}}
Here, TypeScript inferred the return type ofset to bethis, rather thanBox.Now let’s make a subclass ofBox:
tsTryclassClearableBox extendsBox {clear () {this.contents ="";}}consta =newClearableBox ();constb =a .set ("hello");
You can also usethis in a parameter type annotation:
tsTryclassBox {content :string ="";sameAs (other :this) {returnother .content ===this.content ;}}
This is different from writingother: Box — if you have a derived class, itssameAs method will now only accept other instances of that same derived class:
tsTryclassBox {content :string ="";sameAs (other :this) {returnother .content ===this.content ;}}classDerivedBox extendsBox {otherContent :string ="?";}constbase =newBox ();constderived =newDerivedBox ();Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.derived .sameAs (); base
this-based type guards
You can usethis is Type in the return position for methods in classes and interfaces.When mixed with a type narrowing (e.g.if statements) the type of the target object would be narrowed to the specifiedType.
tsTryclassFileSystemObject {isFile ():thisisFileRep {returnthisinstanceofFileRep ;}isDirectory ():thisisDirectory {returnthisinstanceofDirectory ;}isNetworked ():thisisNetworked &this {returnthis.networked ;}constructor(publicpath :string,privatenetworked :boolean) {}}classFileRep extendsFileSystemObject {constructor(path :string,publiccontent :string) {super(path ,false);}}classDirectory extendsFileSystemObject {children :FileSystemObject [];}interfaceNetworked {host :string;}constfso :FileSystemObject =newFileRep ("foo/bar.txt","foo");if (fso .isFile ()) {fso .content ;}elseif (fso .isDirectory ()) {fso .children ;}elseif (fso .isNetworked ()) {fso .host ;}
A common use-case for a this-based type guard is to allow for lazy validation of a particular field. For example, this case removes anundefined from the value held inside box whenhasValue has been verified to be true:
tsTryclassBox <T > {value ?:T ;hasValue ():thisis {value :T } {returnthis.value !==undefined ;}}constbox =newBox <string>();box .value ="Gameboy";box .value ;if (box .hasValue ()) {box .value ;}
Parameter Properties
TypeScript offers special syntax for turning a constructor parameter into a class property with the same name and value.These are calledparameter properties and are created by prefixing a constructor argument with one of the visibility modifierspublic,private,protected, orreadonly.The resulting field gets those modifier(s):
tsTryclassParams {constructor(publicreadonlyx :number,protectedy :number,privatez :number) {// No body necessary}}consta =newParams (1,2,3);console .log (a .x );Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.console .log (a .); z
Class Expressions
Background Reading:
Class expressions (MDN)
Class expressions are very similar to class declarations.The only real difference is that class expressions don’t need a name, though we can refer to them via whatever identifier they ended up bound to:
tsTryconstsomeClass =class<Type > {content :Type ;constructor(value :Type ) {this.content =value ;}};constm =newsomeClass ("Hello, world");
Constructor Signatures
JavaScript classes are instantiated with thenew operator. Given the type of a class itself, theInstanceType utility type models this operation.
tsTryclassPoint {createdAt :number;x :number;y :numberconstructor(x :number,y :number) {this.createdAt =Date .now ()this.x =x ;this.y =y ;}}typePointInstance =InstanceType <typeofPoint >functionmoveRight (point :PointInstance ) {point .x +=5;}constpoint =newPoint (3,4);moveRight (point );point .x ;// => 8
abstract Classes and Members
Classes, methods, and fields in TypeScript may beabstract.
Anabstract method orabstract field is one that hasn’t had an implementation provided.These members must exist inside anabstract class, which cannot be directly instantiated.
The role of abstract classes is to serve as a base class for subclasses which do implement all the abstract members.When a class doesn’t have any abstract members, it is said to beconcrete.
Let’s look at an example:
tsTryabstractclassBase {abstractgetName ():string;printName () {console .log ("Hello, " +this.getName ());}}constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.b =newBase ();
We can’t instantiateBase withnew because it’s abstract.Instead, we need to make a derived class and implement the abstract members:
tsTryclassDerived extendsBase {getName () {return"world";}}constd =newDerived ();d .printName ();
Notice that if we forget to implement the base class’s abstract members, we’ll get an error:
tsTryclassNon-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.extends Derived Base {// forgot to do anything}
Abstract Construct Signatures
Sometimes you want to accept some class constructor function that produces an instance of a class which derives from some abstract class.
For example, you might want to write this code:
tsTryfunctiongreet (ctor :typeofBase ) {constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.instance =newctor ();instance .printName ();}
TypeScript is correctly telling you that you’re trying to instantiate an abstract class.After all, given the definition ofgreet, it’s perfectly legal to write this code, which would end up constructing an abstract class:
tsTry// Bad!greet (Base );
Instead, you want to write a function that accepts something with a construct signature:
tsTryfunctiongreet (ctor :new ()=>Base ) {constinstance =newctor ();instance .printName ();}greet (Derived );Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.greet (); Base
Now TypeScript correctly tells you about which class constructor functions can be invoked -Derived can because it’s concrete, butBase cannot.
Relationships Between Classes
In most cases, classes in TypeScript are compared structurally, the same as other types.
For example, these two classes can be used in place of each other because they’re identical:
tsTryclassPoint1 {x =0;y =0;}classPoint2 {x =0;y =0;}// OKconstp :Point1 =newPoint2 ();
Similarly, subtype relationships between classes exist even if there’s no explicit inheritance:
tsTryclassPerson {name :string;age :number;}classEmployee {name :string;age :number;salary :number;}// OKconstp :Person =newEmployee ();
This sounds straightforward, but there are a few cases that seem stranger than others.
Empty classes have no members.In a structural type system, a type with no members is generally a supertype of anything else.So if you write an empty class (don’t!), anything can be used in place of it:
tsTryclassEmpty {}functionfn (x :Empty ) {// can't do anything with 'x', so I won't}// All OK!fn (window );fn ({});fn (fn );
The TypeScript docs are an open source project. Help us improve these pagesby sending a Pull Request ❤
Last updated: Dec 16, 2025