클래스와 상속
체스 엔진을 만들려고 합니다. 해당 엔진은 두 명이 체스를 둘 수 있는 API를 제공합니다.
먼저 타입부터 정의해 보겠습니다.
// 체스 게임
class Game{}
// 체스 말
class Piece{}
// 체스 말의 좌표 집합
class Position{}
체스에는 여섯 가지의 말(piece)이 있습니다.
// 체스 게임
class Game{}
// 체스 말
class Piece{}
// 체스 말의 좌표 집합
class Position{}
class King extends Piece {} // 킹
class Queen extends Piece{} // 퀸
class Bishop extends Piece{} // 비숍
class Knight extends Piece{} // 나이트
class Rook extends Piece{} // 룩
class Pawn extends Piece{} // 폰
모든 말은 색과 현재 위치 정보를 갖습니다. 체스에선 좌표 쌍(문자, 숫자)으로 말의 위치를 표시합니다.
문자는 x 축을 따라 왼쪽에서 오른쪽으로 증가하며 숫자는 y축을 따라 아래에서 위로 증가합니다.
Piece 클래스에 색과 위치를 추가해 보겠습니다.
type Color = "Black" | "White"
type Files = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H"
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ①
// 체스 말의 좌표 집합
class Position {
constructor(private file: Files, ②
private rank: Rank) {}
}
// 체스 말
class Piece {
protected position: Position ③
constructor(private readonly color: Color, ④
file: Files, rank: Rank) {
this.position = new Position(file, rank)
}
}
// 체스 게임
class Game {}
class King extends Piece {} // 킹
class Queen extends Piece {} // 퀸
class Bishop extends Piece {} // 비숍
class Knight extends Piece {} // 나이트
class Rook extends Piece {} // 룩
class Pawn extends Piece {} // 폰
① 색, 랭크, 파일의 종류가 많지 않으므로 가질 수 있는 모든 값을 타입 리터럴로 직접 열거할 수 있습니다. 모든 문자열과 숫자를 가질 수 있는 이들 타입 도메인에 특정 문자열과 숫자만 가질 수 있도록 제한을 추가함으로써 타입 안전성을 어느 정도 확보할 수 있습니다.
② 생성자의 private 접근 한정자는 자동으로 매개변수를 this에 할당하며, 가시성은 private으로 설정합니다. 즉, Position의 인스턴스 안의 코드는 이 매개변수를 읽고 쓸 수 있지만 Position 인스턴스 외부에선 접근할 수 없습니다. Position 인스턴스끼리는 다른 인스턴스의 비공개(private) 멤버에 접근할 수 있습니다.
③ 인스턴스 변수 position은 protected로 선언했습니다. protected도 private처럼 프로퍼티를 this에 할당하지만 private과 달리 Piece의 인스턴스와 Piece의 서브 클래스 인스턴스 모두에 접근을 허용합니다. position을 선언하면서 할당은 하지 않았으므로 Piece의 생성자 함수에서 값을 할당해야 합니다. 생성자에서도 값을 할당하지 않으면 TypeScript는 변수가 명확하게 할당되지 않았다고 에러 납니다. 이는 변수의 타입을 T라고 선언했지만 실제로는 프로퍼티 초기자나 생성자에서 값을 할당하지 않아 T | undefined 타입으로 결정되므로 position의 타입을 Position 또는 undefined가 될 수 있도록 시그니처를 바꿔야 합니다.
④ new Piece는 color, file, rank 세 개의 매개변수를 받습니다. color에 두 가지 한정자를 추가했는데 private은 color를 this로 할당해서 Piece의 인스턴스에서만 이 변수에 접근할 수 있게 만들며, readonly는 초기에 값을 할당한 다음엔 더 이상 값을 덮어쓸 수 없게 합니다.
TSC 플래그: strictNullChecks와 strictPropertyInitialization
tsconfig.json의 strictNullChecks와 strictPropertyInitialization 플래그를 활성화해서 클래스 인스턴스 변수를 할당했는지 확인할 수 없습니다. strict 플래그를 이미 사용하고 있다면 따로 이들 플래그를 활성화할 필요가 없습니다.
접근 한정자
TypeScript는 클래스의 프로퍼티와 메서드에 세 가지 접근 한정자를 제공합니다.
- public
- 어디에서나 접근할 수 있습니다. 기본적으로 주어지는 접근 수준입니다.
- protected
- 이 클래스와 서브 클래스의 인스턴스에만 접근할 수 있습니다.
- private
- 이 클래스의 인스턴스에만 접근할 수 있습니다.
접근 한정자를 이용해 내부 구현 정보를 너무 많이 공개하지 않고 잘 정의된 API만 노출하도록 클래스를 설계할 수 있습니다.
여기선 Piece 클래스를 정의했는데, 사용자가 Piece 인스턴스를 직접 생성하지 못하게 막고, 대신에 Queen이나 Bishop 등 Piece 클래스를 상속받은 클래스를 통해서만 인스턴스화할 수 있도록 허용할 것입니다.
abstract 키워드를 이용해 타입 시스템이 이 규칙을 강제하도록 설정할 수 있습니다.
// 체스 말
abstract class Piece {
protected position: Position;
constructor(private readonly color: Color, file: Files, rank: Rank) {
this.position = new Position(file, rank);
}
}
Piece를 직접 인스턴스화하려고 시도하면 TypeScript가 아래와 같이 에러를 발생시킵니다.
new Piece('White', 'A', 1) //추상 클래스의 인스턴스를 만들 수 없습니다.ts(2511)
abstract 키워드는 해당 클래스를 바로 인스턴스화할 수 없음을 의미할 뿐 필요한 메서드를 추상 클래스에 자유롭게 추가할 수 있습니다.
// 체스 말
abstract class Piece {
protected position: Position;
constructor(private readonly color: Color, file: Files, rank: Rank) {
this.position = new Position(file, rank);
}
moveTo(position: Position){
this.position = position
}
abstract canMoveTo(position: Position): boolean
}
위의 Piece 클래스는 아래와 같은 의미로 해석할 수 있습니다.
- canMoveTo라는 메서드를 주어진 시그니처와 호환되도록 구현해야 함을 하위 클래스에 알립니다. Piece를 상속받았으나 canMoveTo 메서드를 구현하지 않으면 컴파일 타임에 타입 에러가 발생합니다. 추상 클래스를 구현할 때는 추상 메서드도 반드시 구현해야 합니다.
- moveTo의 기본 구현을 포함합니다. moveTo엔 아무 접근 한정자를 추가하지 않았으므로 기본적으로 public, 즉 다른 모든 코드에서 읽거나 쓰도록 허용합니다.
앞의 조건을 만족하는 canMoveTo를 King 클래스에 구현해 보겠습니다.
두 체스 말의 거리를 쉽게 계산하도록 distanceFrom이라는 유용한 함수도 구현할 것입니다.
type Color = "Black" | "White";
type Files = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H";
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
// 체스 말의 좌표 집합
class Position {
constructor(private file: Files, private rank: Rank) {}
distanceFrom(position: Position) {
return {
rank: Math.abs(position.rank - this.rank),
file: Math.abs(position.file.charCodeAt(0) - this.file.charCodeAt(0))
}
}
}
// 체스 말
abstract class Piece {
protected position: Position;
constructor(private readonly color: Color, file: Files, rank: Rank) {
this.position = new Position(file, rank);
}
moveTo(position: Position){
this.position = position
}
abstract canMoveTo(position: Position): boolean
}
// new Piece('White', 'A', 1) //추상 클래스의 인스턴스를 만들 수 없습니다.ts(2511)
// 체스 게임
class Game {}
class King extends Piece {
canMoveTo(position: Position): boolean {
let distance = this.position.distanceFrom(position)
return distance.rank < 2 && distance.file < 2
}
} // 킹
새 게임을 만들 때 자동으로 보드와 말을 만듭니다.
// 체스 게임
class Game {
private pieces = Game.makePieces()
private static makePieces(){
return {
// Kings
new King('White', 'E', 1),
new King('Black', 'E', 8)
// Queens
new Queen('White', 'D', 1),
new Queen('Black', 'D', 8)
// Bishops
new Bishop('White', 'C', 1),
new Bishop('White', 'F', 1),
new Bishop('Black', 'C', 1),
new Bishop('Black', 'F', 1)
}
}
}
Rank와 Files의 타입을 엄격하게 설정했으므로 'Q' 같은 미리 지정되지 않은 문자나 12 같이 범위를 벗어나는 숫자를 입력하면 TypeScript가 컴파일 타임에 에러를 발생시킵니다.
정리
class 키워드로 클래스를 선언한 후 extends 키워드로 다른 클래스를 상속받을 수 있습니다.
클래스는 구체 클래스와 추상 클래스로 분류됩니다. 추상 클래스는 추상 메서드와 추상 프로퍼티를 가질 수 있습니다.
메서드는 private, protected, public 중 한 가지 한정자를 가질 수 있고 기본값은 public입니다. 메서드는 인스턴스 메서드와 정적 메서드 두 가지로 구분됩니다.
클래스는 인스턴스 프로퍼티도 가질 수 있으며 이 프로퍼티들은 private, protected, public 중 한 가지 한정자를 갖습니다. 생성자의 매개변수나 프로퍼티 초기자에도 이들 한정자를 사용할 수 있습니다.
인스턴스 프로퍼티를 선언할 때 readonly를 추가할 수 있습니다.
'👶 TypeScript' 카테고리의 다른 글
this를 반환 타입으로 사용하기 (0) | 2023.01.15 |
---|---|
super (0) | 2023.01.14 |
타입 주도 개발 (0) | 2023.01.14 |
제네릭 타입 기본값 (0) | 2023.01.14 |
한정된 다형성으로 인수의 개수 정의하기 (0) | 2023.01.14 |