Classes Flashcards

1
Q

What is the most basic class?

A

Here’s the most basic class - an empty one:

class Point {}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

How can you create field in a class?

A

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 well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

How can you initialize a field in a class?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

What do you do if you need to initialize a field externally?

A

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 well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

How can you mark a class field as read-only?

A

With the readonly modifier.

Example:

class Greeter {
  readonly name: string = "world";
  . . .
}

“readonly” (typescriptlang.org). Retrieved April 17, 2023.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

What is a class constructor?

A

The constructor method is a special method of a class for creating and initializing an object instance of that class.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Can you add default values to a class constructor?

A

Yes

Example:

class Point {
  x: number;
  y: number;
 
  // Normal signature with defaults
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Can you overload a class constructor?

A

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
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

What are the differences between class constructor signatures and function signatures?

A

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
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

What are super() calls?

A

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);
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

What is a method?

A

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 well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

How can you add getter and setter accessors to a class?

A

With the get and set accessor keywords:

class C {
  private _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

What happens if a class field as a get accessor but not set accessor?

A

If get exists but no set, the property is automatically readonly

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Is it possible to have accessors with different types for getting and setting?

A

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;
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Is it possible to declare index signatures inside a class?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

How can a class implement an interface?

A

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!");
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
17
Q

How can you extend a class?

A

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!");
    }
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
18
Q

How can you overwride a parent class method?

A

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"
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
19
Q

What are type-only Field Declarations?

A

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);
  }
}
20
Q

Explain the initialization order of a class in JavaScript

A

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.

21
Q

What is the problem with inheriting built-in types in TypeScript if the target is ES6/ES2015?

A

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.

22
Q

How can you “fix” the problem with inheriting built-in types in TypeScript if the target is ES6/ES2015?

A

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.

23
Q

What is the default visibility of a class member (field or method)

A

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.

24
Q

Explain class Member Visibility

A

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.

25
Q

Explain the public modifier

A

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.

26
Q

Explain the private modifier

A

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.

27
Q

Explain the protected modifier

A

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.

28
Q

What is a static member?

A

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();
29
Q

Is it possible to change the visibility of a static member?

A

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'.
30
Q

Are static members inherited?

A

Yes, static members are also inherited:

class Base {
  static getGreeting() {
    return "Hello world";
  }
}

class Derived extends Base {
  myGreeting = Derived.getGreeting();
}
31
Q

What are static blocks in classes?

A

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 {}
    }
}
32
Q

How do yo create a generic class?

A

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>
33
Q

Can you use a generic type in a static member?

A

No. The static members of a generic class can never refer to the class’s type parameters.

34
Q

What is the problem with this at runtime in classes?

A

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.

35
Q

Explain this type in classes

A

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
36
Q

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;
  }
}
A

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'.
*/
37
Q

What is a this based type guard?

A

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.

38
Q

How do you define a this based type guard?

A

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
}
39
Q

When should you use a this based type guard?

A

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.

40
Q

What are class parameter properties?

A

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'.
41
Q

What are parameter properties?

A

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'.
42
Q

What are class expressions?

A

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>
43
Q

Explain abstract classes and members

A

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:

44
Q

What are Abstract Construct Signatures?

A

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.
*/
45
Q

What is the syntax Abstract Construct Signature types?

A
new () => AbstractClassName
46
Q

What is the problem with empty classes?

A

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);