Classes Flashcards
What is the most basic class
?
Here’s the most basic class - an empty one:
class Point {}
“Class Members” (typescriptlang.org). Retrieved April 14, 2023.
How can you create field in a class
?
A field declaration creates a public writeable property on a class:
class Point { x: number; y: number; } const pt = new Point(); pt.x = 0; pt.y = 0;
As with other locations, the type annotation is optional, but will be an implicit any
if not specified.
“Fields” (typescriptlang.org). Retrieved April 14, 2023.
How can you initialize a field in a class
?
Fields can have initializers; these will run automatically when the class is instantiated:
class Point { x = 0; y = 0; } const pt = new Point(); // Prints 0, 0 console.log(`${pt.x}, ${pt.y}`);
Just like with const
, let
, and var
, the initializer of a class property will be used to infer its type.
Fields can also be initalized in the class constructor.
class Point { x: number; y: number; constructor() { this.x = 0; this.y = 0; } }
“Fields” (typescriptlang.org). Retrieved April 14, 2023.
What do you do if you need to initialize a field externally?
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 the definite assignment assertion operator, !
:
class OKGreeter { // Not initialized, but no error name!: string; }
“Fields” (typescriptlang.org). Retrieved April 14, 2023.
How can you mark a class field as read-only?
With the readonly
modifier.
Example:
class Greeter { readonly name: string = "world"; . . . }
“readonly” (typescriptlang.org). Retrieved April 17, 2023.
What is a class constructor?
The constructor method is a special method of a class for creating and initializing an object instance of that class
.
“constructor - JavaScript | MDN” (developer.mozilla.org). Retrieved April 17, 2023.
Can you add default values to a class constructor?
Yes
Example:
class Point { x: number; y: number; // Normal signature with defaults constructor(x = 0, y = 0) { this.x = x; this.y = y; } }
“Constructors” (typescriptlang.org). Retrieved April 21, 2023.
Can you overload a class constructor?
Yes, you can overload it same way as you would overload a function signature.
Example:
class Point { // Overloads constructor(x: number, y: string); constructor(s: string); constructor(xs: any, y?: any) { // TBD } }
“Constructors” (typescriptlang.org). Retrieved April 21, 2023.
What are the differences between class constructor signatures and function signatures?
There are just a few differences between class constructor signatures and function signatures:
- Constructors can’t have type parameters - i.e. They can not declare generic types on the constructor signature, these belong on the outer class declaration.
- Constructors can’t have return type annotations - the class instance type is always what’s returned
“Constructors” (typescriptlang.org). Retrieved May 4, 2023.
What are super()
calls?
Just as in JavaScript, if your class is a derived class, you’ll need to call the parent constructor with super
passing along any arguments that were provided in your constructor body before using any this
members.
Example:
class Base { k; constructr(key: number) { this.k = key; } class Derived extends Base { constructor(key: number) { console.log(this.k); // Error 'super' must be called before accessing 'this' in the constructor of a derived class. super(key); } }
“Constructors” (typescriptlang.org). Retrieved April 21, 2023.
What is a method?
A function property on a class
is called a method. Methods can use all the same type annotations as functions and constructors:
class Point { x = 10; y = 10; scale(n: number): void { this.x *= n; this.y *= n; } }
“Methods” (typescriptlang.org). Retrieved April 17, 2023.
How can you add getter and setter accessors to a class?
With the get
and set
accessor keywords:
class C { private _length = 0; get length() { return this._length; } set length(value) { this._length = value; } }
“Getters / Setters” (typescriptlang.org). Retrieved April 18, 2023.
What happens if a class
field as a get
accessor but not set
accessor?
If get
exists but no set
, the property is automatically readonly
“Getters / Setters” (typescriptlang.org). Retrieved April 18, 2023.
Is it possible to have accessors with different types for getting and setting?
Yes, Since TypeScript 4.3, it is possible to have accessors with different types for getting and setting.
class Thing { _size = 0; get size(): number { return this._size; } set size(value: string | number | boolean) { let num = Number(value); // Don't allow NaN, Infinity, etc if (!Number.isFinite(num)) { this._size = 0; return; } this._size = num; } }
“Getters / Setters” (typescriptlang.org). Retrieved April 18, 2023.
Is it possible to declare index signatures inside a class?
Yes, Classes can declare index signatures; these work the same as Index Signatures for other object types:
class MyClass { [s: string]: boolean | ((s: string) => boolean); check(s: string) { return this[s] as boolean; } }
NOTE: 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.
“Index Signatures” (typescriptlang.org). Retrieved April 18, 2023.
How can a class
implement an interface
?
You can use an implements
clause to check that a class satisfies a particular interface. An error will be issued if a class fails to correctly implement it:
interface Pingable { ping(): void; } class Sonar implements Pingable { ping() { console.log("ping!"); } }
“implements Clauses” (typescriptlang.org). Retrieved April 18, 2023.
How can you extend a class
?
Classes may extend from a base class using the extends
clause. A derived class has all the properties and methods of its base class, and also define additional members.
class Animal { move() { console.log("Moving along!"); } } class Dog extends Animal { woof(times: number) { for (let i = 0; i < times; i++) { console.log("woof!"); } } }
“extends Clauses” (typescriptlang.org). Retrieved April 18, 2023.
How can you overwride a parent class method?
You can override a class method by first extending the base class with a derived class and then redefining the method in the derived class. This is done using the extends
keyword.
// Define the base class class Animal { speak(): void { console.log("The animal makes a sound"); } } // Define the derived class that extends the base class class Dog extends Animal { // Override the speak method in the derived class speak(): void { console.log("The dog barks"); } } // Create a new instance of the derived class and call the overridden method const myDog = new Dog(); myDog.speak(); // Output: "The dog barks" // Create a new instance of the base class and call the original method const myAnimal = new Animal(); myAnimal.speak(); // Output: "The animal makes a sound"
“Overriding Methods” (typescriptlang.org). Retrieved April 18, 2023.
What are type-only Field Declarations?
When target >= ES2022
or useDefineForClassFields
is true
, 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 write declare
to indicate to TypeScript that there should be no runtime effect for this field declaration.
interface Animal { dateOfBirth: any; } interface Dog extends Animal { breed: any; } class AnimalHouse { resident: Animal; constructor(animal: Animal) { this.resident = animal; } } class DogHouse extends AnimalHouse { // Does not emit JavaScript code, // only ensures the types are correct declare resident: Dog; constructor(dog: Dog) { super(dog); } }
“Type-only Field Declarations” (typescriptlang.org). Retrieved April 19, 2023.
Explain the initialization order of a class in JavaScript
In JavaScript, the initialization order of a class is determined by the order in which its constructor function is executed, and the order in which its properties and methods are defined. Here’s a step-by-step explanation of the initialization order in JavaScript:
1.- Class definition: When the JavaScript engine encounters a class definition, it creates a constructor function for the class.
2.- Property and method definitions: After creating the constructor function, the engine adds the methods and properties defined in the class body to the prototype of the constructor function. This ensures that all instances of the class inherit the properties and methods.
3.- Constructor function execution: When a new instance of the class is created using the new
keyword, the constructor function is executed. The this
keyword inside the constructor refers to the newly created instance.
4.- Property initialization: Inside the constructor, you can initialize instance properties with default values or with the values passed as arguments to the constructor function.
5.- Superclass constructor: If the class extends
another class, the constructor of the superclass (parent class) is called using the super()
function. This should be done before any other statements that reference this
in the derived class constructor, as it ensures that the parent class constructor has been executed and the instance has been properly set up.
“Classes - JavaScript | MDN” (developer.mozilla.org). Retrieved April 19, 2023.
What is the problem with inheriting built-in types in TypeScript if the target is ES6/ES2015
?
Inheriting built-in types (like Array
, Map
, Set
, Error
, etc.) in TypeScript can be problematic when the target is set to ES6/ES2015
. This is because there are certain issues with subclassing built-in types in ES6, particularly when it comes to the constructors and how instances are created.
The main issue arises from how ES6 handles the construction of built-in objects internally. When a built-in object constructor is called with new.target
being a derived class, ES6 expects the derived constructor to return an object with the correct internal slots (low-level data structures) for the built-in object. However, TypeScript does not provide a way to create objects with the correct internal slots for the built-in object.
When you try to subclass a built-in type, you may run into issues like:
1- Incorrect prototype chain: The prototype chain of the instance may not be set up correctly, leading to incorrect behavior when interacting with built-in methods or properties. i.e. instanceof
won’t work.
2.- Inability to create instances: When calling the superclass constructor, it may throw an error or fail to create an instance of the subclass correctly.
For example for a subclass like the following:
class MsgError extends Error { constructor(m: string) { super(m); } sayHello() { return "hello " + this.message; } }
you may find that:
methods may be undefined on objects returned by constructing these subclasses, so calling sayHello
will result in an error.instanceof
will be broken between instances of the subclass and their instances, so (new MsgError()) instanceof MsgError
will return false
.
“Inheriting Built-in Types” (typescriptlang.org). Retrieved April 19, 2023.
How can you “fix” the problem with inheriting built-in types in TypeScript if the target is ES6/ES2015?
You need to fix the prototype chain.
For example in a subclass like the following:
class MsgError extends Error { constructor(m: string) { super(m); } sayHello() { return "hello " + this.message; } }
You need to manually adjust the prototype immediately after any super(...)
calls.
class MsgError extends Error { constructor(m: string) { super(m); // Set the prototype explicitly. Object.setPrototypeOf(this, MsgError.prototype); } sayHello() { return "hello " + this.message; } }
However, any subclass of MsgError
will have to manually set the prototype as well. For runtimes that don’t support Object.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 onto this), but the prototype chain itself cannot be fixed.
“Inheriting Built-in Types” (typescriptlang.org). Retrieved April 19, 2023.
What is the default visibility of a class member (field or method)
The default visibility of class members is public
. A public member can be accessed anywhere:
class Greeter { public greet() { console.log("hi!"); } } const g = new Greeter(); g.greet();
Because public
is already the default visibility modifier, you don’t ever need to write it on a class member, but might choose to do so for style/readability reasons.
“Member Visibility” (typescriptlang.org). Retrieved April 20, 2023.
Explain class
Member Visibility
In TypeScript, class member visibility determines the access levels for properties and methods within a class. There are three visibility modifiers: public
, private
, and protected
.
“Member Visibility” (typescriptlang.org). Retrieved April 20, 2023.
Explain the public
modifier
The public
modifier allows class members to be accessible from any part of the code, including outside the class or in derived classes. By default, all class members are public in TypeScript.
Note: Because public
is already the default visibility modifier, you don’t ever need to write it on a class member, but might choose to do so for style/readability reasons.
“Member Visibility” (typescriptlang.org). Retrieved April 20, 2023.
Explain the private
modifier
The private
modifier restricts the access of class members to within the same class only. They cannot be accessed from outside the class or from derived classes.
“Member Visibility” (typescriptlang.org). Retrieved April 20, 2023.
Explain the protected
modifier
The protected
modifier allows class members to be accessed within the same class and in derived classes. However, they cannot be accessed from outside the class hierarchy.
“Member Visibility” (typescriptlang.org). Retrieved April 20, 2023.
What is a static
member?
Classes may have static
members. These members aren’t associated with a particular instance of the class. They can be accessed through the class constructor object itself:
class MyClass { static x = 0; static printX() { console.log(MyClass.x); } } console.log(MyClass.x); MyClass.printX();
“Static Members” (typescriptlang.org). Retrieved May 3, 2023.
Is it possible to change the visibility of a static
member?
Yes, static
members can also use the same public
, protected
, and private
visibility modifiers:
class MyClass { private static x = 0; } console.log(MyClass.x); // Error, Property 'x' is private and only accessible within class 'MyClass'.
“Static Members” (typescriptlang.org). Retrieved May 3, 2023.
Are static
members inherited?
Yes, static
members are also inherited:
class Base { static getGreeting() { return "Hello world"; } } class Derived extends Base { myGreeting = Derived.getGreeting(); }
“Static Members” (typescriptlang.org). Retrieved May 3, 2023.
What are 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.
class Foo { static #count = 0; get count() { return Foo.#count; } static { try { const lastInstances = loadLastInstances(); Foo.#count += lastInstances.length; } catch {} } }
“static Blocks in Classes” (typescriptlang.org). Retrieved May 3, 2023.
How do yo create a generic class
?
Classes can use generic constraints and defaults the same way as interfaces.
class Box<Type> { contents: Type; constructor(value: Type) { this.contents = value; } } const b = new Box("hello!"); const b: Box<string>
“Generic Classes” (typescriptlang.org). Retrieved May 3, 2023.
Can you use a generic
type in a static
member?
No. The static
members of a generic class can never refer to the class’s type parameters.
“Type Parameters in Static Members” (typescriptlang.org). Retrieved May 3, 2023.
What is the problem with this
at runtime in classes?
JavaScript’s handling of this
is unusual:
class MyClass { name = "MyClass"; getName() { return this.name; } } const c = new MyClass(); const obj = { name: "obj", getName: c.getName, }; // Prints "obj", not "MyClass" console.log(obj.getName());
Long story short, by default, the value of this
inside a function depends on how the function was called. In this example, because the function was called through the obj
reference, its value of this was obj
rather than the class instance.
At runtime this
will be whatever is at the left of the dot.
“this at Runtime in Classes” (typescriptlang.org). Retrieved May 3, 2023.
Explain this
type in class
es
In classes, a special type called this
refers dynamically to the type of the current class.
Example:
class Box { contents: string = ""; set(value: string) { this.contents = value; return this; // Return type of `set` method is inferred as `this` type } } class ClearableBox extends Box { clear() { this.contents = ""; } } const a = new ClearableBox(); const b = a.set("hello"); // Type of b ClearableBox
“this Types” (typescriptlang.org). Retrieved May 4, 2023.
What happens if you use a this
in a parameter type annotation?
Example:
class Box { content: string = ""; sameAs(other: this) { return other.content === this.content; } }
Using this
type is different from writing other: Box
— if you have a derived class, its sameAs
method will now only accept other instances of that same derived class:
class Box { content: string = ""; sameAs(other: this) { return other.content === this.content; } } class DerivedBox extends Box { otherContent: string = "?"; } const base = new Box(); const derived = new DerivedBox(); derived.sameAs(base); /* Error: Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'. */
“this Types” (typescriptlang.org). Retrieved May 4, 2023.
What is a this
based type guard?
A this
based type guard is a special type of type guard in TypeScript that allows you to narrow the type of this
within a class method, based on the method’s return type.
“this Types” (typescriptlang.org). Retrieved May 4, 2023.
How do you define a this
based type guard?
To define a this
based type guard, use the this is TypeName
syntax as the return type of a method within a class. This allows you to narrow the type of this
in the scope where the method returns true.
Example:
class FileSystemObject { isFile(): this is FileRep { return this instanceof FileRep; } isDirectory(): this is Directory { return this instanceof Directory; } isNetworked(): this is Networked { return this.networked; } constructor(public path: string, private networked: boolean) {} } class FileRep extends FileSystemObject { constructor(path: string, public content: string) { super(path, false); } } class Directory extends FileSystemObject { children: FileSystemObject[]; } interface Networked { host: string; } const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo"); if (fso.isFile()) { fso.content; // const fso: FileRep } else if (fso.isDirectory()) { fso.children; // const fso: Directory } else if (fso.isNetworked()) { fso.host; // const fso: Networked & FileSystemObject }
“this Types” (typescriptlang.org). Retrieved May 4, 2023.
When should you use a this
based type guard?
Use a this
based type guard when you want to narrow the type of the current class instance (this
) within a method, and when you want to check if the current instance has certain properties or methods specific to a derived class.
“this Types” (typescriptlang.org). Retrieved May 4, 2023.
What are class parameter properties
?
TypeScript offers special syntax for turning a constructor parameter into a class property with the same name and value. These are called parameter properties and are created by prefixing a constructor argument with one of the visibility modifiers public
, private
, protected
, or readonly
. The resulting field gets those modifier(s):
class Params { constructor( public readonly x: number, protected y: number, private z: number ) { // No body necessary } } const a = new Params(1, 2, 3); console.log(a.x); // (property) Params.x: number console.log(a.z); // Error: Property 'z' is private and only accessible within class 'Params'.
“Parameter Properties” (typescriptlang.org). Retrieved May 4, 2023.
What are class expressions?
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:
const someClass = class<Type> { content: Type; constructor(value: Type) { this.content = value; } }; const m = new someClass("Hello, world"); // Typeof m is someClass<string>
“Class Expressions” (typescriptlang.org). Retrieved May 5, 2023.
Explain abstract
classes and members
Classes, methods, and fields in TypeScript may be abstract
.
An abstract method or abstract field is one that hasn’t had an implementation provided. These members must exist inside an abstract 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 be concrete.
Example:
abstract class Base { abstract getName(): string; printName() { console.log("Hello, " + this.getName()); } } const b = new Base(); // Error: Cannot create an instance of an abstract class. class Derived extends Base { getName() { return "world"; } } const d = new Derived(); d.printName();
Note: beware that if we forget to implement the base class’s abstract members, we’ll get an error:
“abstract Classes and Members” (typescriptlang.org). Retrieved May 5, 2023.
What are Abstract Construct Signatures?
They are a way to define a variable or argument type as a derived class of an abstract class.
Example, given this abstract and derived classes:
abstract class Base { abstract getName(): string; printName() { console.log("Hello, " + this.getName()); } } class Derived extends Base { getName() { return "world"; } }
If you wanted to write a fn that instantiates a class that extends Base
class and calls printName
method. You may try to do this code:
function greet(ctor: typeof Base) { const instance = new ctor(); // Error: Cannot create an instance of an abstract class. instance.printName(); }
TypeScript is correctly telling you that you’re trying to instantiate an abstract class. After all, given the definition of greet, it’s perfectly legal to write this code, which would end up constructing an abstract class:
Instead, you want to write a function that accepts something with a construct signature:
function greet(ctor: new () => Base) { const instance = new ctor(); instance.printName(); } greet(Derived); greet(Base); /* Error: 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. */
“Abstract Construct Signatures” (typescriptlang.org). Retrieved May 5, 2023.
What is the syntax Abstract Construct Signature types?
new () => AbstractClassName
What is the problem with empty classes?
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:
class Empty {} function fn(x: Empty) { // can't do anything with 'x', so I won't } // All OK! fn(window); fn({}); fn(fn);
“Relationships Between Classes” (typescriptlang.org). Retrieved May 5, 2023.