# ⚪️ TypeScript Advanced Knowledge

# Type Aliases

Type aliases are used to give a new name to a type, often used with union types.

type Name = string;
//Type Alias Name: This creates a type alias NameResolver for a function that takes no parameters and returns a string. It represents a function that resolves a name.
type NameResolver = () => string;
//Type Alias NameOrResolver: NameOrResolver is a union type alias that can be either a Name (string) or a NameResolver (function returning string). This allows flexibility in accepting different types for the getName function.
type NameOrResolver = Name | NameResolver;
//Function getName: The getName function takes a parameter x of type NameOrResolver. If x is a string, it's returned directly. If it's a function, the function is invoked to get the name. This function illustrates the flexibility provided by type aliases.
function getName(x: NameOrResolver) {
  if (typeof x === "string") {
    return x;
  }
  return x();
}

# Advantages:

  • Readability:

    • Type aliases allow you to give meaningful names to types, improving code readability. In this example, Name, NameResolver, and NameOrResolver make the code more self-explanatory.
  • Reusability:

    • Type aliases promote the reuse of types throughout your codebase. If you need to represent a name or a function that resolves a name in multiple places, using type aliases avoids redundancy.
  • Flexibility:

    • By combining different types into a union (NameOrResolver), you gain flexibility in function parameters. This flexibility allows the getName function to accept either a name or a name resolver function.

# String Literal Types

Restrict values to a specific set of string literals.

type EventNames = "click" | "scroll" | "mousemove";
function handleEvent(el: Element, event: EventNames) {
  // do something
}

handleEvent(document.getElementById("hello") as Element, "scroll");
handleEvent(document.getElementById("hello") as Element, "jump"); // error

# Tuples

Arrays store elements of the same type, while tuples store elements of different types.

  1. Tuple types allow you to express an array with a fixed number of elements whose types are known, but need not be the same.
  2. When accessing or modifying elements with a known index, the correct type is returned.
  3. During initialization, all internal elements must be included unless the element is marked as "optional."

# Accessing Tuple Elements

const john: [string, number] = ["John", 30];
let kevin: [string, number?];

// 1.
john[0] = "johnny dept";
john[1] = "100"; // error

// 2.
kevin = ["Kevin"]; // Initializing with missing elements will result in an error unless the element is optional.

# Tuple Overflow

When adding elements beyond the original tuple limit, the type is restricted to the union type of each type in the tuple.

let tom: [string, number] = ["Tom", 25];
tom.push("male");
tom.push(true);
// Type 'boolean' is not assignable to type 'string | number'. ts(2345)

# Enum

Enums are used to define a set of named constants, making it easier to document or identify code. Sure, here are the explanations in English:

# Incremental Enum:

In TypeScript, an incremental enum refers to a numeric enum where each member's value automatically increases. Here's an example:

// Numeric Enum (increases by default)
enum Weekday {
  Monday, // 0
  Tuesday, // 1
  Wednesday, // 2
  Thursday, // 3
  Friday, // 4
  Saturday, // 5
  Sunday, // 6
}

// Usage
let today = Weekday.Wednesday;
console.log(today); // Outputs: 2

In this example, Weekday is a numeric enum, and its members automatically increase starting from 0. If no value is specified for a member, TypeScript increments it based on the value of the preceding member.

If you manually set a value for a member and want subsequent members to increment automatically, you can do it like this:

enum Weekday {
  Monday = 1, // 1
  Tuesday, // 2
  Wednesday, // 3
  Thursday, // 4
  Friday, // 5
  Saturday, // 6
  Sunday, // 7
}

Here, Monday is manually set to 1, and the following members will increment accordingly. This kind of enum can enhance code clarity and readability in certain situations.

# String Enum:

String enums in TypeScript are enums where each member has an associated string value. They are beneficial for serialization and debugging because the runtime values are meaningful and readable. Here's an example:

// String Enum
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// Usage
let myDirection = Direction.Left;
console.log(myDirection); // Outputs: "LEFT"

# heterogeneous enum

In this example, Direction is a string enum where each member has an associated string value. This allows for more meaningful representations during runtime, especially when debugging or serializing enum values. Heterogeneous Enums refer to enums where the member values have different data types. This situation typically occurs in enums that mix numeric and string members. Here's an example of a heterogeneous enum:

enum Status {
  Success = 200,
  NotFound = "Not Found",
  Error = "Internal Server Error",
}

// Usage
let successStatus: Status = Status.Success;
let notFoundStatus: Status = Status.NotFound;
let errorStatus: Status = Status.Error;

console.log(successStatus); // Outputs: 200
console.log(notFoundStatus); // Outputs: "Not Found"
console.log(errorStatus); // Outputs: "Internal Server Error"

In this example, the Status enum has three members, each with a different data type for its value. One member has a numeric value (Success), and the other two have string values (NotFound and Error). This allows the enum to represent status codes or messages with different data types, and you can choose the appropriate member based on the context. The usage of heterogeneous enums is relatively less common and is typically employed when handling data of different types simultaneously.

# interface enum

In TypeScript, enum is commonly used to define a set of named numeric constants, while interface is used to define the structure of objects. These two concepts are typically used for different purposes, where one represents a group of related constants, and the other represents the shape of objects.

Here is an example using both enum and interface:

// Define a set of direction constants using enum
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// Define the structure of an object with coordinates and direction using interface
interface Point {
  x: number;
  y: number;
  direction: Direction; // Using constants from the enum
}

// Create an object adhering to the Point interface using the defined enum
const point: Point = {
  x: 1,
  y: 2,
  direction: Direction.Right,
};

console.log(point);

# const enum

TypeScriptにおけるenumとconst enumの違いを、tscのコンパイル結果から確認してみる

enumとconstsの使い分け

# Reverse mapping

Reverse mapping refers to the capability in an enum type where, in addition to the normal mapping from names to values, there is also a mapping from enum values back to their names. This feature provides more flexibility in certain programming scenarios.

In TypeScript, when you assign initial values to enum members, TypeScript automatically generates both forward mapping (from enum names to values) and reverse mapping (from enum values to names). Here's an example:

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// Forward mapping
console.log(Direction.Up); // Output: UP

// Reverse mapping
console.log(Direction["UP"]); // Output: Up

In this example, the value of the enum member Direction.Up is the string "UP". Through Direction["UP"], you can retrieve the name of this member, which is "Up". This illustrates the concept of reverse mapping, where you find the corresponding name based on the enum value.

Reverse mapping can be useful in various scenarios, such as generating labels on the user interface based on enum values or determining enum values based on user-inputted strings.

In this example, enum Direction defines four constants representing different directions, and then interface Point defines the structure of an object with x, y, and direction properties, where direction uses constants from the Direction enum. Finally, an object point is created that conforms to the structure defined by the Point interface.

# Classes

While JavaScript has the concept of classes, many JavaScript programmers may not be very familiar with classes. Here's a brief introduction to class-related concepts:

  • Class: Defines the abstract characteristics of a thing, including its properties and methods.

  • Object: An instance of a class, created through the new keyword.

  • Object-Oriented Programming (OOP) Three Pillars: Encapsulation, Inheritance, and Polymorphism.

  • Encapsulation: Hides the details of data operations, exposing only the necessary interfaces. It ensures that external callers can interact with the object through provided interfaces without knowing the internal details.

  • Inheritance: A mechanism where a subclass inherits properties and behaviors from a superclass. The subclass retains the characteristics of the superclass and may have additional features.

  • Polymorphism: Different classes related through inheritance can respond to the same method in different ways. For example, a Cat and a Dog both inheriting from an Animal class may have different implementations of the eat method.

  • Accessors (Getter & Setter): Methods to change the reading and assigning behavior of properties.

  • Modifiers (Access Modifiers): Keywords that limit the nature of members or types, such as public indicating a public property or method.

  • Abstract Class: A base class meant for other classes to inherit from. Instances of an abstract class cannot be created, and abstract methods within it must be implemented by its subclasses.

  • Interfaces: Common properties or methods shared among different classes, abstracted into an interface. Classes can implement (or adhere to) multiple interfaces.

# readonly

The readonly keyword is used to declare read-only properties and can only appear in property declarations or index signatures.

class Animal {
    readonly name;
    public constructor(name) {
        this.name = name;
    }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

Note that if readonly is used along with other access modifiers, it should come after them.

class Animal {
    // public readonly name;
    public constructor(public readonly name) {
        this.name = name;
    }
}

# Instance Properties

In ES7 proposals, instance properties can be defined directly inside the class.

class Animal {
  name = "Jack";
}

# Static Properties

class Animal {
  static num = 42;
}

# Access Modifiers

In TypeScript, access modifiers include public, private, and protected.

  • public: Accessible anywhere (default). The modified property or method is public, meaning it can be accessed from any part of the code. All properties and methods are public by default.

  • private: Cannot be accessed outside the declaring class. The modified property or method is private, and it cannot be accessed from outside the class where it is declared.

  • protected: Accessible within the declaring class and its subclasses. The modified property or method is protected, similar to private. However, it can be accessed within the declaring class and its subclasses.

class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}

let dog = new Animal("Cute");
console.log(dog.name); // 'name' is a private property and can only be accessed within class 'Animal'.
dog.name = "Tom"; // 'name' is a private property and can only be accessed within class 'Animal'.

Note: private does not restrict access in the compiled code; it provides compile-time checking only.

# Getters and Setters

class User {
  // Private property to store the password
  private _password: string = "******";

  // Getter method to retrieve the password
  get password(): string {
    return this._password;
  }

  // Setter method to update the password
  set password(newPass: string) {
    this._password = newPass;

    // When printing the password in the setter, use this.password instead of u-password
    console.log(this.password);
  }
}

// Example usage
const u = new User();

// Get the current password
const currentPassword = u.password;
console.log(currentPassword); // Output: ******

// Set a new password
u.password = "newPassword";

# Abstract Classes

abstract is used to define abstract classes and abstract methods, and instances cannot be created from them.

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

let a = new Animal("Jack");

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

In the example above, we define an abstract class Animal and an abstract method sayHi. Attempting to instantiate an abstract class directly results in an error.

# Implementing Abstract Methods

Secondly, abstract methods within an abstract class must be implemented by subclasses:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}

let cat = new Cat("Tom");

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

In this example, the Cat class inherits from Animal but fails to implement the abstract method sayHi, resulting in a compilation error.

Here's a correct usage example:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}

let cat = new Cat("Tom");

In this corrected example, we implement the sayHi method in the Cat class, making it a valid subclass of the abstract Animal class.

It's important to note that even though it's an abstract method, TypeScript still generates the corresponding class in the compiled result, as seen in the generated JavaScript code.

Note: Abstract classes are present in the compiled code.

# Here are a few common scenarios where abstract classes are beneficial in TypeScript:

Abstract classes in TypeScript serve the primary purpose of providing a base class for deriving other classes. They can include both concrete implementations and abstract members, which must be implemented in derived classes. Abstract classes cannot be instantiated on their own; they are meant to be used as base classes for other classes.

  1. Sharing Implementation Logic: Abstract classes can contain shared implementation logic that may be common across derived classes. By placing this logic in the abstract class, redundancy in code can be avoided.

    abstract class Shape {
      abstract calculateArea(): number;
    
      displayArea(): void {
        console.log(`Area: ${this.calculateArea()}`);
      }
    }
    
    class Circle extends Shape {
      constructor(private radius: number) {
        super();
      }
    
      calculateArea(): number {
        return Math.PI * this.radius ** 2;
      }
    }
    
    const circle = new Circle(5);
    circle.displayArea(); // Outputs: Area: 78.54
    
  2. Enforcing Specific Interface Implementation: Abstract members in an abstract class must be implemented in derived classes. This helps ensure that derived classes have specific behavior or properties.

    abstract class Printer {
      abstract printDocument(document: string): void;
    }
    
    class LaserPrinter extends Printer {
      printDocument(document: string): void {
        console.log(`Printing document (laser): ${document}`);
      }
    }
    
    class InkjetPrinter extends Printer {
      printDocument(document: string): void {
        console.log(`Printing document (inkjet): ${document}`);
      }
    }
    
  3. Providing a Common Interface: Abstract classes can define a set of common methods or properties to ensure a consistent interface across derived classes.

    abstract class Animal {
      abstract makeSound(): void;
    
      move(): void {
        console.log("Moving...");
      }
    }
    
    class Dog extends Animal {
      makeSound(): void {
        console.log("Woof! Woof!");
      }
    }
    
    class Bird extends Animal {
      makeSound(): void {
        console.log("Chirp! Chirp!");
      }
    }
    

In summary, abstract classes in TypeScript are a tool for building class hierarchies and providing a consistent interface. They help establish stricter relationships between classes and ensure certain behaviors remain consistent throughout the class hierarchy.

# Class and Interface Interactions

# Class Implements Interface

When different classes share common features, interfaces can be used, and classes implement them.

interface Chatroom {
  connect();
}

class Customer {}

class CustomA extends Customer implements Chatroom {
  connect() {
    console.log("welcome to A");
  }
}

class CustomB extends Customer implements Chatroom {
  connect() {
    console.log("welcome to B");
  }
}

A class can implement multiple interfaces:

interface Chatroom {
  connect();
}

interface Shop {
  buy();
}

class Customer {}

class Custom extends Customer implements Chatroom, Shop {
  connect() {
    console.log("welcome~");
  }
  buy() {
    console.log("buy successful");
  }
}

For more interface and class interaction patterns, check here (opens new window).

# generators and iterators

In TypeScript, generators and iterators are often used together to provide a convenient way to implement iterable objects. Let's discuss generators, iterators, and how to use them in TypeScript.

# Iterators

In TypeScript, an iterator is an object with a next method. The next method returns an object with value and done properties, indicating the current value of the iteration and whether it is done.

interface Iterator<T> {
  next(): { value: T; done: boolean };
}

// Example: Iterator for a range of numbers
function createNumberRangeIterator(
  start: number,
  end: number
): Iterator<number> {
  let current = start;
  return {
    next(): { value: number; done: boolean } {
      const result = { value: current, done: current > end };
      current++;
      return result;
    },
  };
}

const numberIterator = createNumberRangeIterator(1, 3);
console.log(numberIterator.next()); // { value: 1, done: false }
console.log(numberIterator.next()); // { value: 2, done: false }
console.log(numberIterator.next()); // { value: 3, done: false }
console.log(numberIterator.next()); // { value: undefined, done: true }

# Generators

Generators are a special type of function declared using function*. They can pause execution using yield to return a value and can later resume execution. Generator functions return an iterator.

// Example: Generator for a range of numbers
function* createNumberRangeGenerator(
  start: number,
  end: number
): Generator<number> {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const numberGenerator = createNumberRangeGenerator(1, 3);
console.log(numberGenerator.next()); // { value: 1, done: false }
console.log(numberGenerator.next()); // { value: 2, done: false }
console.log(numberGenerator.next()); // { value: 3, done: false }
console.log(numberGenerator.next()); // { value: undefined, done: true }

# Using Generators and Iterators

Generators offer a more concise and readable syntax. You can use the for...of loop to iterate over the values generated by the generator.

// Using a generator to create a number range
function* createNumberRangeGenerator(
  start: number,
  end: number
): Generator<number> {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

// Iterating over the generator
for (const num of createNumberRangeGenerator(1, 3)) {
  console.log(num);
}
// Output:
// 1
// 2
// 3

Generators and iterators provide a flexible and clear way to handle a sequence of values, especially when dealing with large datasets or asynchronous operations.

# Generics

Generics allow you to define functions, interfaces, or classes without specifying the exact type, and instead, determine the type during usage.

# Basic Usage

Let's start with a function, createArray, that creates an array of a specified length and fills each element with a default value:

function createArray(length: number, value: any): Array<any> {
  let result = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray(3, "x"); // Result: ['x', 'x', 'x']

The issue here is that the return type (Array<any>) allows any type in the array. To address this, we introduce generics:

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray<string>(3, "x"); // Result: ['x', 'x', 'x']

Now, the function is generic, and you can specify the type (e.g., string) when calling it.

# Multiple Type Parameters

Define multiple type parameters in a generic function, like in the swap function:

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

swap([7, "seven"]); // Result: ['seven', 7]

Here, swap takes a tuple and swaps its elements.

# Generic Constraints

When using generic variables inside a function, you might need to constrain them. For example:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity({ length: 10 }); // Prints the length of the object

Here, loggingIdentity has a generic constraint to ensure that the type T extends the Lengthwise interface, which has a length property.

# Generic Interface

Interfaces can also be generic. Here's an example:

interface CreateArrayFunc {
  <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function <T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
};

createArray(3, "x"); // Result: ['x', 'x', 'x']

You can also move the generic type parameter to the interface level.

# Generic Class

Like interfaces, classes can also use generics:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

This GenericNumber class can work with various numeric types.

# Default Generic Types

Starting TypeScript 2.3, you can specify default types for generic parameters:

function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

This allows you to provide a default type (string in this case) when a specific type is not explicitly provided.

Generics enhance the flexibility and type safety of your code, allowing you to create reusable and type-agnostic components. They are particularly useful when you want to create functions or classes that can work with a variety of data types.

# More Generics

const echo = <T>(arg: T): T => arg;

const isObj = <T>(arg: T): boolean => {
  return typeof arg === "object" && !Array.isArray(arg) && arg !== null;
};

console.log(isObj(true)); // false
console.log(isObj("John")); // false
console.log(isObj([1, 2, 3])); // false
console.log(isObj({ name: "John" })); // true
console.log(isObj(null)); // false

////////////////////////////////////

const isTrue = <T>(arg: T): { arg: T; is: boolean } => {
  if (Array.isArray(arg) && !arg.length) {
    return { arg, is: false };
  }
  if (isObj(arg) && !Object.keys(arg as keyof T).length) {
    return { arg, is: false };
  }
  return { arg, is: !!arg };
};

console.log(isTrue(false)); //{ arg: false, is: false }
console.log(isTrue(0)); //{ arg: 0, is: false }
console.log(isTrue(true)); //{ arg: true, is: true }
console.log(isTrue(1)); //{ arg: 1, is: true }
console.log(isTrue("Dave"));
console.log(isTrue(""));
console.log(isTrue(null));
console.log(isTrue(undefined));
console.log(isTrue({})); // modified
console.log(isTrue({ name: "Dave" }));
console.log(isTrue([])); // modified
console.log(isTrue([1, 2, 3]));
console.log(isTrue(NaN));
console.log(isTrue(-0));

////////////////////////////////////

# Common Techniques

# Extracting Variable Types

Use typeof to extract variable types:

let a = 123;
let b = { x: 0, y: 1 };

type A = typeof a; // number
type B = typeof b; // { x: number, y: number }

# Binding Function this

Bind this on the first parameter. See reference (opens new window)

const obj = {
  say(name: string) {
    console.log("Hello: ", name);
  },
};

function test(this: typeof obj, str: string) {
  console.log(this.say(str));
}

# Index Variables

interface A {
  [key: string]: any;
}

// 'in' iterates over sub-properties, resulting in the type: string
type B = {
  [key in "a" | "b" | "c"]: string;
};

# Built-in Types

TypeScript provides built-in utility types:

# Record

Generates an object type with keys of type K and values of type T.

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

const foo: Record<string, boolean> = {
  a: true,
};

const bar: Record<"x" | "y", number> = {
  x: 1,
  y: 2,
};

# Partial

Makes all properties of T optional.

type Partial<T> = {
  [P in keyof T]?: T[P];
};

interface Foo {
  a: string;
  b: number;
}

const foo: Partial<Foo> = {
  b: 2, // 'a' is optional
};

# Required

Opposite of Partial, makes all properties of T required.

# Readonly

Makes all properties of T readonly.

# Pick

Selects properties from T specified by K.

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

interface Foo {
  a: string;
  b: number;
  c: boolean;
}

const foo: Pick<Foo, "b" | "c"> = {
  b: 1,
  c: false,
};

# Exclude

Exclude from T those types that are assignable to U.

type Exclude<T, U> = T extends U ? never : T;

let foo: Exclude<"a" | "b" | "c", "b"> = "a";
foo = "c";

# Extract

Extract from T those types that are assignable to U.

type Extract<T, U> = T extends U ? T : never;

let foo: Extract<"a" | "b" | "c", "b"> = "b";

# Parameters

Returns a tuple type based on the parameters of a function.

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

type Foo = (a: string, b: number) => void;
const a: Parameters<Foo> = ["a", 1]; // [string, number]

# ReturnType

Returns the return type of a function.

type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

type Foo = () => boolean;
const a: ReturnType<Foo> = true; // Returns boolean type

# 🔹 index signature

// Index Signatures

// interface TransactionObj {
//     readonly [index: string]: number
// }

interface TransactionObj {
  readonly [index: string]: number;
  Pizza: number;
  Books: number;
  Job: number;
}

const todaysTransactions: TransactionObj = {
  Pizza: -10,
  Books: -5,
  Job: 50,
};

console.log(todaysTransactions.Pizza);
console.log(todaysTransactions["Pizza"]);

let prop: string = "Pizza";
console.log(todaysTransactions[prop]);

const todaysNet = (transactions: TransactionObj): number => {
  let total = 0;
  for (const transaction in transactions) {
    total += transactions[transaction];
  }
  return total;
};

console.log(todaysNet(todaysTransactions));

//todaysTransactions.Pizza = 40
console.log(todaysTransactions["Dave"]); // undefined

///////////////////////////////////

interface Student {
  [key: string]: string | number | number[] | undefined;
  name: string;
  GPA: number;
  classes?: number[];
}

const student: Student = {
  name: "Doug",
  GPA: 3.5,
  classes: [100, 200],
};

console.log(student.test); //because of the index signature, this is allowed

for (const key in student) {
  console.log(`${key}: ${student[key as keyof Student]}`); //'name: Doug' ,'GPA: 3.5' ​​​​​, 'classes: 100,200' ​​​​​, 'test: undefined'
}

Object.keys(student).map((key) => {
  console.log(student[key as keyof typeof student]); // 'Doug' , 3.5 , [ 100, 200 ]
});

const logStudentKey = (student: Student, key: keyof Student): void => {
  console.log(`Student ${key}: ${student[key]}`); //'Student name: Doug'
};

logStudentKey(student, "name");

/////////////////////////////////

// interface Incomes {
//     [key: string]: number
// }

type Streams = "salary" | "bonus" | "sidehustle";

type Incomes = Record<Streams, number>;

const monthlyIncomes: Incomes = {
  salary: 500,
  bonus: 100,
  sidehustle: 250,
};

for (const revenue in monthlyIncomes) {
  console.log(monthlyIncomes[revenue as keyof Incomes]); // 500 , 100 , 250
}

🔺 student[key as keyof typeof student]:

Inside the function passed to map, each key is used to access the corresponding value in the student object. - key is a variable holding the current property name being iterated over. - key as keyof typeof student is a TypeScript type assertion. It tells TypeScript to treat key as one of the keys of the type of student. This is necessary because, by default, the type of key would be just string, which is too general. The keyof typeof student type is more specific, representing the union of all literal types of the keys in the student object.

Object.keys(student).map((key) => {
  console.log(student[key as keyof typeof student]);
});

🔺 補充理解在 JavaScript 和 TypeScript 中,对象属性可以通过两种方式访问

点符号(.)和方括号符号([])。在您提供的代码中,todaysTransactions 是一个对象,其中包含了不同的属性(如 PizzaBooksJob),每个属性都有对应的值。

  1. 点符号: todaysTransactions.Pizza。这种方式是直接访问对象的属性,其中 Pizza 是属性的字面量名称。

  2. 方括号符号: todaysTransactions['Pizza']todaysTransactions[prop]。这种方式用于当属性名是动态的或者是一个变量时。在这种情况下,属性名需要作为字符串传递。

在代码中,prop 是一个字符串类型的变量,其值为 'Pizza'。因此,当你使用 todaysTransactions[prop] 时,它实际上是 todaysTransactions['Pizza'] 的简写。由于 prop 变量的值是 'Pizza',所以这个表达式最终访问的是 todaysTransactions 对象中 Pizza 这个属性的值。

# Accessing object properties dynamically in TypeScript

Accessing object properties dynamically in TypeScript can be achieved through various methods, including dot notation, square brackets, and the keyof operator. Each method has its specific use cases and benefits depending on the requirements of your application.

  1. Using Square Brackets: This method is particularly useful when the property name is dynamic, such as in the case of user input or other runtime conditions. For example, you might have a TypeScript React component where you need to access properties of an object based on user interactions or input. Here's a simple example:

    interface User {
      name: string;
      age: number;
      email: string;
    }
    
    const user: User = {
      name: "Alice",
      age: 25,
      email: "alice@example.com",
    };
    
    const propertyName = "email";
    const propertyValue = user[propertyName];
    console.log(propertyValue); // Outputs: alice@example.com
    

    In this example, propertyName is a variable that holds the name of the property you want to access. This approach is useful in scenarios like handling form inputs dynamically.

  2. Using keyof with TypeScript: The keyof operator is another powerful feature in TypeScript, which you can use to create a union type of all the keys in an object. This is particularly useful for ensuring type safety when accessing properties dynamically. Here's an example:

    interface User {
      name: string;
      age: number;
      email: string;
    }
    
    type UserKeys = keyof User;
    
    const user: User = {
      name: "Bob",
      age: 30,
      email: "bob@example.com",
    };
    
    const propertyName: UserKeys = "name";
    const propertyValue = user[propertyName];
    console.log(propertyValue); // Outputs: Bob
    

    In this example, propertyName is a variable of type UserKeys, which is a union of all keys in the User interface. This ensures that only valid property names can be assigned to propertyName.

  3. Dynamic Property Access in React Components: In React components, you might encounter scenarios where you need to dynamically access or update the state based on user input or interaction. For example, if you are building a form with multiple fields, you can use the square bracket notation to update the state for each field dynamically:

    import React, { useState } from "react";
    
    const FormComponent: React.FC = () => {
      const [formState, setFormState] = useState<{ [key: string]: string }>({});
    
      const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setFormState({
          ...formState,
          [event.target.name]: event.target.value,
        });
      };
    
      return (
        <form>
          <input name="username" onChange={handleChange} />
          <input name="email" onChange={handleChange} />
          {/* ... other form fields ... */}
        </form>
      );
    };
    

    In this React component, handleChange function updates the formState dynamically based on the input field's name attribute.

🔺 For more detailed information, you can refer to these resources:

# Reference Articles

  1. TypeScript for Beginners (opens new window)
  2. TypeScript Practical Tips and Tricks (opens new window)