러닝 타입스크립트 8장 ( 클래스 )
포스트
취소

러닝 타입스크립트 8장 ( 클래스 )

해당 포스트는 러닝 타입스크립트 8장을 읽고 정리한 포스트입니다.
책의 모든 내용을 작성하는 것이 아닌 주관적인 기준에 따라 필요한 정보만 정리했습니다.

클래스의 키워드 작성 순서는 접근성 / static / readonly 순서로 작성해야 합니다.
( protected static readonly age: number = 20; )

🔱 클래스 메서드

클래스의 생성자는 클래스의 메서드처럼 취급됩니다.
그리고 메서드는 일반 함수와 마찬가지로 매개변수와 반환 값에 대한 타입 체크를 수행합니다.
( noImplicitAny를 해제한 경우에는 매개변수의 타입을 지정하지 않으면 any가 됩니다. )

1
2
3
4
5
6
7
8
class Person {
  constructor(name: string) {}

  say(message: string){}
}

const person = new Person("");
person.say("");

💸 클래스 속성

클래스의 속성을 읽거나 쓰려면 클래스의 최상단에 명시적으로 선언해야 합니다.
하지만 타입 애너테이션은 선택적으로 사용해도 됩니다.
( 사용하지 않으면 생성자에서 할당되는 초깃값을 기준으로 결정되는 것 같습니다. )

(1)과 같이 정의하지 않은 멤버에 접근하려고 하면 오류가 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
  name;
  // name: string;

  constructor(name: string) {
    this.name = name;
  }

  say(message: string){}
}

const person = new Person("");
person.say("");

// (1) Error: Property 'age' does not exist on type 'Person'
person.age;

0️⃣ 함수 속성

  • 메서드: 클래스의 프로토타입으로 함수를 할당하는 방식
  • 속성으로 선언한 메서드: 클래스의 속성으로 함수를 할당하는 방식

메서드는 인스턴스끼리 공유하는 함수가 되고, 속성으로 선언한 메서드는 각 인스턴스가 갖는 독립적인 함수가 됩니다.
( this로 각자의 멤버에 접근할 수 있기 때문에 굳이 속성으로 선언할 필요는 없다고 생각합니다. )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
  move;

  constructor () {
    // 속성
    this.move = () => {};
  }

  // 메서드
  say() {}
}

const person1 = new Person();
const person2 = new Person();

console.log(person1.move === person2.move); // false
console.log(person1.say === person2.say); // true

1️⃣ 초기화 검사

strictNullChecks가 켜져있다면 아래와 같이 초기화를 하지 않은 경우 타입 에러를 보여줍니다.

1
2
3
4
5
6
class Person {
  // Error: Property 'name' has no initializer and is not definitely assigned in the constructor
  name: string;

  // constructor () {}
}

만약 strictNullChecks를 켜두지 않았다면 아래와 같이 타입 체커에서는 통과하지만 런타임에 문제가 되는 동작을 허용할 수 있습니다.
따라서 strictNullChecks는 켜두는 것이 좋습니다.

1
2
3
4
5
6
7
8
class Person {
  name: string;

  // constructor () {}
}

// 문제 없이 동작 ( 하지만 실제로는 "name"이 존재하지 않음 )
new Person().name.length;

1. 확실하게 할당된 속성

만약 strictNullChecks를 켜뒀지만, 초기화 검사를 하고 싶지 않은 경우 Not-null assertion(!)을 사용하면 됩니다.

1
2
3
4
5
class Person {
  name!: string;

  // constructor () {}
}

2️⃣ 선택적 속성

클래스는 선언된 속성 이름 뒤에 ?를 사용해서 옵셔널로 선언합니다.

1
2
3
4
5
6
7
8
9
10
class Person {
  name?: string;

  // constructor () {}
}

const person = new Person();

// myName: string | undefined
const { name: myName } = person;

3️⃣ 읽기 전용 속성

인터페이스처럼 클래스에서도 readonly를 추가해 속성을 읽기 전용으로 선언할 수 있습니다.
readonly를 사용해서 선언한 멤버는 선언된 위치((1)) 혹은 생성자((2))에서만 값을 할당할 수 있습니다.

( readonlyTypeScript 전용 문법이기 때문에 컴파일 후에 사라집니다. )
( 혹시 보호의 목적이라면 private, getter를 사용하는 것이 맞습니다. )

1
2
3
4
5
6
7
8
9
10
11
class Person {
  // (1)
  readonly name: string = "Aatrox";

  constructor () {
    // (2)
    this.name = "Puppy";
  }
}

const person = new Person();

원시 타입을 갖는 readonly의 초깃값은 타입 추론이 구체적으로 됩니다.
letconst에서 추론하듯이 리터럴 타입으로 추론됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
  name1 = "Aatrox";
  readonly name2 = "Aatrox";
  readonly name3: string = "Aatrox";

  constructor() {
    // 정상 동작
    this.name1 = "Puppy";

    // Error: Type '"Puppy"' is not assignable to type '"Aatrox"'
    this.name2 = "Puppy";

    // 정상 동작
    this.name3 = "Puppy";
  }
}

// person.name1: string
// person.name2: "Aatrox"
// person.name3: string
const person = new Person();

🧧 타입으로서의 클래스

클래스는 JavaScript의 값으로 사용되지만, TypeScript의 타입으로 사용할 수 있습니다.

흥미로운 점은 해당 클래스의 인스턴스가 아니더라도 모든 멤버와 메서드를 갖고 있다면 할당할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
  name = "Aatrox";

  constructor() {}

  say() {}
}

// 타입으로 사용한 클래스
const person: Person = {
  // 인스턴스가 아니더라도 "Person"에 정의된 멤버와 메서드를 갖고 있는 객체라면 할당 가능
  name: "",
  say() {},
};

💎 클래스와 인터페이스

implements를 사용하면 해당 클래스가 해당 인터페이스를 준수한다고 선언할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
  name: string;
  say(): void;
}

// Class 'Student' incorrectly implements interface 'Person'.
// Property 'say' is missing in type 'Student' but required in type 'Person'.
class Student implements Person {
  name = "Aatrox";

  constructor() {}

  // say() {}
}

해당 인터페이스를 준수하지 않으면 타입 에러가 발생하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Person {
  name: string;
  say(message: string): void;
}

class Student implements Person {
  name = "Aatrox";

  constructor() {}

  /**
   * Property 'say' in type 'Student' is not assignable to the same property in base type 'Person'.
   * Type '(message: number) => void' is not assignable to type '(message: string) => void'.
   *   Types of parameters 'message' and 'message' are incompatible.
   *     Type 'string' is not assignable to type 'number'.
   */
  say(message: number) {}
}

0️⃣ 다중 인터페이스 구현

implements 뒤에 ,를 이용해서 개수 제한 없이 인터페이스를 등록할 수 있습니다.
해당 클래스는 등록된 모든 인터페이스를 준수하도록 정의되어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person1 {
  name: string;
}
interface Person2 {
  age: number;
}

class Student implements Person1, Person2 {
  name = "Aatrox";
  age = 26;

  constructor() {}
}

만약 인터페이스끼리 중복된 부분이 있고 그 타입이 겹칠 수 없다면 해당 클래스는 정의할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Person1 {
  name: string;
}
interface Person2 {
  name: string | number;
  age: number;
}

class Student implements Person1, Person2 {
  name = "Aatrox";

  // Property 'name' in type 'Student' is not assignable to the same property in base type 'Person1'.
  // Type 'number' is not assignable to type 'string'.
  // name = 0;
  
  age = 26;

  constructor() {}
}

⛳ 클래스 확장

extends를 이용하면 클래스의 관계가 생기고 확장할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Student extends Person {
  age: number;

  constructor(name: string, age: number) {
    super(name);
    this.age = age;
  }
}

0️⃣ 할당 가능성 확장

하위 클래스에서 기보 클래스의 멤버나 메서드를 사용할 수 있습니다.

구조적 타이핑을 따르기 때문에 기보 클래스의 타입에 하위 클래스의 인스턴스를 할당할 수 있습니다.
(1)에서는 실제로 값은 들어가 있지만 타입 체커는 값이 없다고 판단하기 때문에 타입 에러가 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Student extends Person {
  age: number;

  constructor(name: string, age: number) {
    super(name);
    this.age = age;
  }
}

const student = new Student("Aatrox", 26);
const person: Person = new Student("Aatrox", 26);

student.name;
// (1) Error: Property 'age' does not exist on type 'Person'.
person.age;

1️⃣ 재정의된 생성자

하위 클래스의 생성자에서는 반드시 최상위에서 기본 클래스의 생성자를 호출((2))해야 합니다.

기본 클래스나 하위 클래스가 아무것도 받지 않으면 생략해도 됩니다.
( 생성자를 생략하면 기본적인 형태의 생성자가 들어간 것처럼 동작합니다. )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Student extends Person {
  age: number;

  constructor(name: string, age: number) {
    // (2)
    super(name);
    this.age = age;
  }
}

2️⃣ 재정의된 메서드 ( Overriding )

하위 클래스에서는 기본 클래스의 메서드를 재정의할 수 있습니다.

하위 클래스에서 재정의한 메서드는 기본 클래스에서도 사용할 수 있어야 하기 때문에 매개변수와 반환 타입이 같거나 더 구체적이어야 합니다.
(3)의 경우는 volume이 추가되긴 했지만, 옵셔널이기 때문에 기본 클래스에서도 문제 없이 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  say(message: string) {
    console.log(message)
  }
}

class Student extends Person {
  age: number;

  constructor(name: string, age: number) {
    super(name);
    this.age = age;
  }

  // (3)
  say(message: string, volume?: number) {
    console.log(message)
  }
}

3️⃣ 재정의된 속성

속성도 마찬가지로 기본 클래스에서도 사용할 수 있어야 하기 때문에 구조적으로 일치해야 합니다.

(4)처럼 다른 타입을 지정하면 타입 에러가 발생하게 됩니다.
또한 하위 클래스에서 재정의하는 것이기 때문에 하위 클래스에서 초기화해줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
  name?: string;

  constructor(name?: string) {
    this.name = name;
  }
}

class Student extends Person {
  name: string;

  // (4) Property 'name' in type 'Student' is not assignable to the same property in base type 'Person'.
  // Type 'number' is not assignable to type 'string'
  // name: number;

  constructor(name: string) {
    super();
    this.name = name;
  }
}

🤿 추상 클래스

클래스, 메서드, 멤버를 직접 구현하고 싶지 않고 형태만 정의하고 싶을 수 있습니다.
그런 경우에 추상화된 기본 클래스만 만드는 목적으로 사용하면 좋은 기능입니다.

추상 클래스로 인스턴스를 만들 수 없고, 추상 메서드와 추상 멤버는 반드시 하위 클래스에서 구현해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 추상 클래스
abstract class Person {
  // 추상 멤버
  abstract name: string;
  // 추상 메서드
  abstract say(): void;

  // constructor() {};
}

class Student extends Person {
  name: string;

  constructor(name: string) {
    super();
    this.name = name;
  }

  say() {}
}

// Error: Cannot create an instance of an abstract class
const person = new Person("Aatrox");

const student = new Student("Aatrox");

🥯 멤버 접근성

TypeScript에서는 접근 제한자가 존재합니다.
JavaScript에서도 #이라는 private한 접근 제한자가 존재하는데 private#은 같은 역할을 하지만 다릅니다.

#은 컴파일 후에도 사라지지 않는 JavaScript 고유의 문법이고, private은 컴파일 후에 사라지는 TypeScript 고유의 문법입니다.

  • 접근 제한자
    1. public: 어디서나 접근 가능
    2. protected: 클래스 내부 또는 하위 클래스에서만 접근 가능
    3. private: 클래스 내부에서만 접근 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
abstract class Person {
  public name = "Aatrox";
  protected age = 26;
  private gender = true;

  constructor(name: string, age: number, gender: boolean) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  };

  abstract say(): void
}

class Student extends Person {
  constructor(name: string, age: number, gender: boolean) {
    super(name, age, gender);
  }

  say(){
    console.log(this.name);
    console.log(this.age);
    // // Error: Property 'gender' is private and only accessible within class 'Person'.
    // console.log(this.gender);
  }
}

const student = new Student("Puppy", 13, false);

student.name;
// Error: Property 'age' is protected and only accessible within class 'Person' and its subclasses
student.age;
// Error: Property 'gender' is private and only accessible within class 'Person'
student.gender;

student.say(); // "Puppy" 13

0️⃣ 정적 필드 제한자

클래스 자체의 멤버로 선언하기 위해 사용하는 키워드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
  public static age = 26;
  private static gender = true;

  public static say() {
    console.log(Person.age, Person.gender);
  }
}

const person = new Person();

// Error: Property 'age' does not exist on type 'Person'.
person.age;

Person.age;
Person.age = 20;

// Error: Property 'gender' is private and only accessible within class 'Person'.
Person.gender;

Person.say(); // 20, true

📮 레퍼런스

  1. « 러닝 타입스크립트 8장 » ( 조시 골드버그 지음, 고승원 옮김, 한빛미디어, 2023 )
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

러닝 타입스크립트 7장

러닝 타입스크립트 9장 ( 타입 제한자 )