1. 클래스와 상속
- class 키워드로 클래스를 선언하고 extends 키워드로 다른 클래스를 상속받을 수 있다.
- 클래스 종류 : 구체 클래스, 추상 클래스
- 추상 클래스: abstract로 선언하며, 추상 메서드와 추상 프로퍼티를 가질 수 있다. 추상 클래스는 바로 인스턴스화 할 수 없다. 다른 클래스처럼 필요한 메서드는 자유롭게 추가해 사용할 수 있다.
- 메서드는 private, protected, public 중 하나의 한정자를 가질 수 있고, 기본값은 public이다. 접근 한정자를 통해 내부 구현 정보를 너무 많이 공개하지 않고 특정 API만 노출하도록 클래스를 설계할 수 있다. 메서드는 인스턴스 메서드와 정적 메서드로 구분할 수 있다.
- private: 해당 클래스의 인스턴스에서만 접근 가능하다. private은 자동으로 매개변수를 this에 할당한다.
- protected: 해당 클래스와 이를 상속받은 서브 클래스의 인스턴스에서만 접근 가능하다.
- public: 어디에서나 접근 가능하다. 기본값.
- 클래스는 인스턴스 프로퍼티를 가질 수 있으며, 이 프로퍼티도 한정자를 가진다. constructor의 매개변수에도 한정자를 사용할 수 있다.
- 인스턴스 프로퍼티를 선언할 때 readonly를 사용할 수 있다. readonly는 초기에 값을 할당한 다음에는 더 이상 값을 덮어쓸 수 없게 한다.
위 사항들을 코드를 통해 확인해보자.
두 명이 체스를 둘 수 있는 체스 엔진을 만드려고 한다. 먼저 클래스와 타입을 정의한다.
체스 게임 : class Game {}
체스 말: class Piece {}
체스 말의 좌표 집합 : class Position {}
type Color = "Black" | "White";
type FileType = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H";
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
// 체스 말의 좌표 집합
class Position {
constructor(private file: FileType, private rank: Rank) {}
}
// 체스 말
class Piece {
protected position: Position;
constructor(private readonly color: Color, file: FileType, rank: Rank) {
this.position = new Position(file, rank);
}
}
사용자가 Piece를 직접 생성하지 못하게 막고, 대신 Piece 클래스를 상속받은 King 또는 Queen 과 같은 서브 클래스만 인스턴스화 할 수 있도록 허용하려고 한다. 이를 위해 Piece를 추상 클래스로 만들어 규칙을 강제하도록 할 수 있다.
abstract class Piece {
protected position: Position;
constructor(private readonly color: Color, file: FileType, rank: Rank) {
this.position = new Position(file, rank);
}
// position으로 이동
moveTo(position: Position) {
this.position = position;
}
abstract canMoveTo(position: Position): boolean; // 추상 클래스
}
추상 클래스는 canMoveTo와 같은 추상 메서드를 가질 수 있다. 추상 클래스를 구현할 때(상속 받아 사용)는 반드시 추상 메서드를 구현해야 한다. 그렇지 않으면 에러가 발생한다.
반면, moveTo는 서브 클래스에서 오버라이드 하여 쓸 수 있지만, 하지 않아도 상관 없다.
class King extends Piece {
canMoveTo(position: Position) {
let distance = this.position.distanceFrom(position);
return distance.rank < 2 && distance.file < 2;
}
}
위에서 만든 게임 말들과 위치 집합을 이용해 다음과 같이 게임을 생성할 수 있다.
class Game {
private pieces = Game.makePieces();
private static makePieces() {
return [
// 킹
new King("White", "E", 1),
new King("Black", "E", 8),
// 퀸
new Queen("White", "D", 1),
new Queen("Black", "D", 8),
// 비숍
new Bishop("White", "C", 1),
new Bishop("White", "F", 1),
new Bishop("Black", "C", 8),
new Bishop("Black", "F", 8),
];
}
}
2. super
- 자식 클래스가 부모 클래스에 정의된 메서드를 오버라이드 하는 경우(즉, 덮어 쓰는 경우)에 super를 통해 부모 버전의 메서드를 호출할 수 있다.
- constructor에서 super 호출 가능. 자식 클래스에서 constructor를 사용하는 경우, super()를 호출해야 부모 클래스와 정상적으로 연결이 된다.
- super로는 부모 클래스의 메서드에만 접근할 수 있고, 프로퍼티에는 접근할 수 없다.
3. this를 반환 타입으로 사용하기
- this는 값 뿐 아니라 타입으로도 사용 가능하다. 클래스를 상속해 사용하는데 각 클래스가 자신의 인스턴스를 반환해야 하는 경우 유용하게 사용된다.
// this를 사용하지 않은 경우
class Set {
has(value: number): boolean {
//...
}
add(vlaue: number): Set {
//...
}
}
class MutableSet extends Set {
delete(value: number): boolean {
// ...
}
add(value: number): MutableSets {
// ...
}
// this를 사용하는 경우
class Set {
has(value: number): boolean {
//...
}
add(vlaue: number): this {}
}
4. 인터페이스
- 타입 별칭과 인터페이스는 문법만 다를 뿐 거의 같은 기능을 수행한다.
// 타입 별칭
type Sushi = {
calories: number;
salty: boolean;
tasty: boolean;
};
// 인터페이스
interface Sushi {
calories: number;
salty: boolean;
tasty: boolean;
}
- 타입과 인터페이스의 차이
- 타입 별칭은 더 일반적이다. 따라서 타입 별칭에는 타입 표현식(타입과 &, | 등의 타입 연산자)을 포함한 모든 타입을 할당할 수 있다. 반면, 인터페이스는 반드시 형태를 할당해야 한다.
- 인터페이스는 상속 시에 상속 받는 타입에 상위 인터페이스를 할당 가능한지 확인한다.
- 타입 별칭은 같은 이름으로 정의하면 에러가 발생하지만, 인터페이스는 이름과 범위가 같은 경우 이들이 자동으로 합쳐진다. (선언 합침)
type Sushi = {
calories: number;
salty: boolean;
tasty: boolean;
};
interface Sushi {
calories: number;
salty: boolean;
tasty: boolean;
}
// 1. 타입 별칭은 인터페이스와 비교해 더 일반적이다.
type A = number;
type B = A | string;
// 2. 인터페이스 상속 시 상속 받는 타입에 상위 인터페이스를 할당 가능한지 확인한다.
interface C {
good(x: number): string;
bad(x: number): string;
}
interface D extends C {
good(x: string | number): string;
bad(x: string): string; // D가 C를 잘못 상속하고 있음. bad의 매개변수 타입이 호환 x
}
// 3. 선언 합침
interface User {
name: string;
}
interface User {
age: number;
}
// User는 name, age의 두 개의 필드를 가짐
let user1: User = {
name: "Amy",
age: 30,
};
// 중복된 식별자 'User'
type User = {
name: string
}
4-1. 선언 합침
- 같은 이름으로 정의된 여러 정의를 자동으로 합치는 타입스크립트의 기능
- 예를 들어 User 라는 똑같은 이름의 인터페이스를 두 개 정의하면 자동으로 하나의 인터페이스로 합친다.
interface User {
name: string;
}
interface User {
age: number;
}
// 선언 합침(declaration merging)
let a: User = {
name: "Ashley",
age: 20,
};
- 인터페이스끼리는 충돌해서는 안된다. 동일한 이름의 인터페이스를 정의할 경우, 다른 프로퍼티 선언(서로 다른 인터페이스 내)도 같은 타입을 가져야 한다.
4-2. 구현
- 클래스 선언 시 implements 키워드로 특정 인터페이스를 만족해야 함을 표시
- 인터페이스를 사용하면 구현에 문제가 있을 때 어디가 잘못되었는지 쉽게 파악할 수 있다. 구현 시 클래스는 인터페이스가 선언하는 모든 메서드와 프로퍼티를 반드시 구현해야 하며, 필요하다면 메서드나 프로퍼티를 추가적으로 구현할 수 있다.
- 프로퍼티의 경우, 접근 제한자는 사용할 수 없지만 readonly는 가능하다.
- 어댑터, 팩토리, 전략 등의 디자인 패턴을 구현하는 대표적 방식이기도 하다.
interface Animal {]
// 추상 메서드
eat(food: string): void;
sleep(hours: number): void;
// 인스턴스 프로퍼티
readonly name: string
}
class Cat implements Animal {
name = "Hello"
eat(food: string) {
console.info(food)
}
sleep(hours: number) {
console.log(hours)
}
}
- 한 클래스가 여러 인터페이스를 구현할 수 있다.
interface Animal {]
// 추상 메서드
eat(food: string): void;
sleep(hours: number): void;
// 인스턴스 프로퍼티
readonly name: string
}
interface Catty {
meow() : void
}
class Cat implements Animal, Catty {
name = "Hello"
eat(food: string) {
console.info(food)
}
sleep(hours: number) {
console.log(hours)
}
meow() {
console.log('meow')
}
}
4-3. 인터페이스 구현 vs. 추상 클래스 상속
인터페이스
- 인터페이스가 더 범용으로 쓰이고 가볍다.
- 인터페이스는 타입의 형태를 정의하는 수단이다. 값 수준에서 객체, 배열, 함수, 클래스, 클래스 인스턴스를 정의할 수 있다는 뜻이다.
- 자바스크립트 코드를 만들지 않고, 컴파일 타임에만 존재한다.
- 모든 메서드가 추상 메서드이다.
- "이 클래스는 T이다"라고 할 때
추상 클래스
- 특별한 목적과 풍부한 기능을 가진다.
- 오직 클래스만 정의할 수 있다.
- 런타임의 자바스크립트 클래스 코드를 만든다.
- 생성자와 기본 구현을 가질 수 있으며, 프로퍼티와 메서드에 접근 제한자를 지정할 수 있다.
- 여러 클래스에서 공유하는 구현인 경우 사용
5. 클래스는 구조 기반 타입을 지원한다.
- 타입스크립트는 클래스를 비교할 때 이름이 아니라 구조를 기준으로 삼는다.
- 즉, 클래스는 같은 프로퍼티와 메서드를 정의하는 기존의 일반 객체를 포함해 클래스 형태를 공유하는 다른 모든 타입을 할당할 수 있다.
- 하지만 protected나 private 필드를 갖는 클래스의 경우, 해당 클래스와 서브클래스의 인스턴스만 할당할 수 있다.
class First {
private x = 1
}
class Second extends First {
}
function f(a: First){}
f(new First) // OK !
f(new Second) // OK !
f({x : 1}) // First 타입에서 x는 private이지만 여기서는 아님.
6. 클래스는 값과 타입을 모두 선언한다.
- 값과 타입은 타입스크립트에서 다른 네임스페이스에 존재한다. 일반적으로 아래와 같이 타입스크립트가 알아서 값이나 타입으로 추론한다.
// 값
let a = 1000;
function b() {}
// 타입
type c = number;
interface d {}
- 클래스와 열거형은 타입 네임스페이스에 타입을, 값 네임스페이스에 값을 동시에 생성한다는 점에서 특별하다.
class C {}
let x: C = // 클래스의 인스턴스 타입
new C // 클래스 값
enum E { F, G }
let e: E = // 열거형 타입
E.F // 열거형 값
- 클래스와 열거형은 타입 수준에서 타입을 생성하기 때문에 'is-a' 관계를 쉽게 표현할 수 있다.
- is-a 관계는 'A(ex. 사과)는 B(ex.과일)이다' 처럼 카테고리 구별을 위한 관계이다. 이는 상속을 표현할 때 사용된다.
- 반면 'has-a' 관계는 'A는 B를 가지고 있다' 처럼 어떤 기능을 포함하는 관계이다. 클래스를 다른 클래스의 멤버 변수로 사용하는 경우이다.
- 클래스 선언 코드의 타입은 아래와 같이 인스턴스 타입과 생성자 타입을 합친 것이라고 볼 수 있다. 클래스 정의는 값 수준과 타입 수준에서 해석되고, 타입 수준에서는 두 가지로 타입으로 정의된다.
type State = {
[key: string]: string;
};
class StringDataBase {
state: State = {};
get(key: string): string | null {
return key in this.state ? this.state[key] : null;
}
set(key: string, value: string): void {
this.state[key] = value;
}
static from(state: State) {
let db = new StringDataBase();
for (let key in state) {
db.set(key, state[key]);
}
return db;
}
}
/**
* 클래스는 값 수준과 타입 수준 모두에서 정의된다.
* 클래스는 타입 수준에서 두 가지 타입을 생성한다.
* 1. 클래스의 인스턴스 타입
* 2. 클래스 생성자(constructor) 타입
*/
// 1. 클래스의 인스턴스 타입
interface StringDataBase {
state: State;
get(key: string): string | null;
set(key: string, value: string): void;
}
// 2. 클래스 생성자 타입
interface StringDatabasConstructor {
new (state: State): StringDataBase; // 생성자 시그니처 : new 연산자로 해당 타입을 인스턴스화 할 수 있음을 정의
setFlagsFromString(state: State): StringDataBase;
}
'FrontEnd > TypeScript' 카테고리의 다른 글
[TypeScript] 비동기 프로그래밍, 동시성과 병렬성 (0) | 2023.06.07 |
---|---|
[TypeScript] 에러 처리 (0) | 2023.06.05 |
[TypeScript] 가변성(슈퍼타입/서브타입 파악하기) & 할당성 & 타입 넓히기 (0) | 2023.06.03 |
[TypeScript] 함수의 타입 (0) | 2023.06.03 |
[TypeScript/타입스크립트 프로그래밍] 타입스크립트 타입 선언 & 종류 (2) | 2023.05.25 |
1. 클래스와 상속
- class 키워드로 클래스를 선언하고 extends 키워드로 다른 클래스를 상속받을 수 있다.
- 클래스 종류 : 구체 클래스, 추상 클래스
- 추상 클래스: abstract로 선언하며, 추상 메서드와 추상 프로퍼티를 가질 수 있다. 추상 클래스는 바로 인스턴스화 할 수 없다. 다른 클래스처럼 필요한 메서드는 자유롭게 추가해 사용할 수 있다.
- 메서드는 private, protected, public 중 하나의 한정자를 가질 수 있고, 기본값은 public이다. 접근 한정자를 통해 내부 구현 정보를 너무 많이 공개하지 않고 특정 API만 노출하도록 클래스를 설계할 수 있다. 메서드는 인스턴스 메서드와 정적 메서드로 구분할 수 있다.
- private: 해당 클래스의 인스턴스에서만 접근 가능하다. private은 자동으로 매개변수를 this에 할당한다.
- protected: 해당 클래스와 이를 상속받은 서브 클래스의 인스턴스에서만 접근 가능하다.
- public: 어디에서나 접근 가능하다. 기본값.
- 클래스는 인스턴스 프로퍼티를 가질 수 있으며, 이 프로퍼티도 한정자를 가진다. constructor의 매개변수에도 한정자를 사용할 수 있다.
- 인스턴스 프로퍼티를 선언할 때 readonly를 사용할 수 있다. readonly는 초기에 값을 할당한 다음에는 더 이상 값을 덮어쓸 수 없게 한다.
위 사항들을 코드를 통해 확인해보자.
두 명이 체스를 둘 수 있는 체스 엔진을 만드려고 한다. 먼저 클래스와 타입을 정의한다.
체스 게임 : class Game {}
체스 말: class Piece {}
체스 말의 좌표 집합 : class Position {}
type Color = "Black" | "White";
type FileType = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H";
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
// 체스 말의 좌표 집합
class Position {
constructor(private file: FileType, private rank: Rank) {}
}
// 체스 말
class Piece {
protected position: Position;
constructor(private readonly color: Color, file: FileType, rank: Rank) {
this.position = new Position(file, rank);
}
}
사용자가 Piece를 직접 생성하지 못하게 막고, 대신 Piece 클래스를 상속받은 King 또는 Queen 과 같은 서브 클래스만 인스턴스화 할 수 있도록 허용하려고 한다. 이를 위해 Piece를 추상 클래스로 만들어 규칙을 강제하도록 할 수 있다.
abstract class Piece {
protected position: Position;
constructor(private readonly color: Color, file: FileType, rank: Rank) {
this.position = new Position(file, rank);
}
// position으로 이동
moveTo(position: Position) {
this.position = position;
}
abstract canMoveTo(position: Position): boolean; // 추상 클래스
}
추상 클래스는 canMoveTo와 같은 추상 메서드를 가질 수 있다. 추상 클래스를 구현할 때(상속 받아 사용)는 반드시 추상 메서드를 구현해야 한다. 그렇지 않으면 에러가 발생한다.
반면, moveTo는 서브 클래스에서 오버라이드 하여 쓸 수 있지만, 하지 않아도 상관 없다.
class King extends Piece {
canMoveTo(position: Position) {
let distance = this.position.distanceFrom(position);
return distance.rank < 2 && distance.file < 2;
}
}
위에서 만든 게임 말들과 위치 집합을 이용해 다음과 같이 게임을 생성할 수 있다.
class Game {
private pieces = Game.makePieces();
private static makePieces() {
return [
// 킹
new King("White", "E", 1),
new King("Black", "E", 8),
// 퀸
new Queen("White", "D", 1),
new Queen("Black", "D", 8),
// 비숍
new Bishop("White", "C", 1),
new Bishop("White", "F", 1),
new Bishop("Black", "C", 8),
new Bishop("Black", "F", 8),
];
}
}
2. super
- 자식 클래스가 부모 클래스에 정의된 메서드를 오버라이드 하는 경우(즉, 덮어 쓰는 경우)에 super를 통해 부모 버전의 메서드를 호출할 수 있다.
- constructor에서 super 호출 가능. 자식 클래스에서 constructor를 사용하는 경우, super()를 호출해야 부모 클래스와 정상적으로 연결이 된다.
- super로는 부모 클래스의 메서드에만 접근할 수 있고, 프로퍼티에는 접근할 수 없다.
3. this를 반환 타입으로 사용하기
- this는 값 뿐 아니라 타입으로도 사용 가능하다. 클래스를 상속해 사용하는데 각 클래스가 자신의 인스턴스를 반환해야 하는 경우 유용하게 사용된다.
// this를 사용하지 않은 경우
class Set {
has(value: number): boolean {
//...
}
add(vlaue: number): Set {
//...
}
}
class MutableSet extends Set {
delete(value: number): boolean {
// ...
}
add(value: number): MutableSets {
// ...
}
// this를 사용하는 경우
class Set {
has(value: number): boolean {
//...
}
add(vlaue: number): this {}
}
4. 인터페이스
- 타입 별칭과 인터페이스는 문법만 다를 뿐 거의 같은 기능을 수행한다.
// 타입 별칭
type Sushi = {
calories: number;
salty: boolean;
tasty: boolean;
};
// 인터페이스
interface Sushi {
calories: number;
salty: boolean;
tasty: boolean;
}
- 타입과 인터페이스의 차이
- 타입 별칭은 더 일반적이다. 따라서 타입 별칭에는 타입 표현식(타입과 &, | 등의 타입 연산자)을 포함한 모든 타입을 할당할 수 있다. 반면, 인터페이스는 반드시 형태를 할당해야 한다.
- 인터페이스는 상속 시에 상속 받는 타입에 상위 인터페이스를 할당 가능한지 확인한다.
- 타입 별칭은 같은 이름으로 정의하면 에러가 발생하지만, 인터페이스는 이름과 범위가 같은 경우 이들이 자동으로 합쳐진다. (선언 합침)
type Sushi = {
calories: number;
salty: boolean;
tasty: boolean;
};
interface Sushi {
calories: number;
salty: boolean;
tasty: boolean;
}
// 1. 타입 별칭은 인터페이스와 비교해 더 일반적이다.
type A = number;
type B = A | string;
// 2. 인터페이스 상속 시 상속 받는 타입에 상위 인터페이스를 할당 가능한지 확인한다.
interface C {
good(x: number): string;
bad(x: number): string;
}
interface D extends C {
good(x: string | number): string;
bad(x: string): string; // D가 C를 잘못 상속하고 있음. bad의 매개변수 타입이 호환 x
}
// 3. 선언 합침
interface User {
name: string;
}
interface User {
age: number;
}
// User는 name, age의 두 개의 필드를 가짐
let user1: User = {
name: "Amy",
age: 30,
};
// 중복된 식별자 'User'
type User = {
name: string
}
4-1. 선언 합침
- 같은 이름으로 정의된 여러 정의를 자동으로 합치는 타입스크립트의 기능
- 예를 들어 User 라는 똑같은 이름의 인터페이스를 두 개 정의하면 자동으로 하나의 인터페이스로 합친다.
interface User {
name: string;
}
interface User {
age: number;
}
// 선언 합침(declaration merging)
let a: User = {
name: "Ashley",
age: 20,
};
- 인터페이스끼리는 충돌해서는 안된다. 동일한 이름의 인터페이스를 정의할 경우, 다른 프로퍼티 선언(서로 다른 인터페이스 내)도 같은 타입을 가져야 한다.
4-2. 구현
- 클래스 선언 시 implements 키워드로 특정 인터페이스를 만족해야 함을 표시
- 인터페이스를 사용하면 구현에 문제가 있을 때 어디가 잘못되었는지 쉽게 파악할 수 있다. 구현 시 클래스는 인터페이스가 선언하는 모든 메서드와 프로퍼티를 반드시 구현해야 하며, 필요하다면 메서드나 프로퍼티를 추가적으로 구현할 수 있다.
- 프로퍼티의 경우, 접근 제한자는 사용할 수 없지만 readonly는 가능하다.
- 어댑터, 팩토리, 전략 등의 디자인 패턴을 구현하는 대표적 방식이기도 하다.
interface Animal {]
// 추상 메서드
eat(food: string): void;
sleep(hours: number): void;
// 인스턴스 프로퍼티
readonly name: string
}
class Cat implements Animal {
name = "Hello"
eat(food: string) {
console.info(food)
}
sleep(hours: number) {
console.log(hours)
}
}
- 한 클래스가 여러 인터페이스를 구현할 수 있다.
interface Animal {]
// 추상 메서드
eat(food: string): void;
sleep(hours: number): void;
// 인스턴스 프로퍼티
readonly name: string
}
interface Catty {
meow() : void
}
class Cat implements Animal, Catty {
name = "Hello"
eat(food: string) {
console.info(food)
}
sleep(hours: number) {
console.log(hours)
}
meow() {
console.log('meow')
}
}
4-3. 인터페이스 구현 vs. 추상 클래스 상속
인터페이스
- 인터페이스가 더 범용으로 쓰이고 가볍다.
- 인터페이스는 타입의 형태를 정의하는 수단이다. 값 수준에서 객체, 배열, 함수, 클래스, 클래스 인스턴스를 정의할 수 있다는 뜻이다.
- 자바스크립트 코드를 만들지 않고, 컴파일 타임에만 존재한다.
- 모든 메서드가 추상 메서드이다.
- "이 클래스는 T이다"라고 할 때
추상 클래스
- 특별한 목적과 풍부한 기능을 가진다.
- 오직 클래스만 정의할 수 있다.
- 런타임의 자바스크립트 클래스 코드를 만든다.
- 생성자와 기본 구현을 가질 수 있으며, 프로퍼티와 메서드에 접근 제한자를 지정할 수 있다.
- 여러 클래스에서 공유하는 구현인 경우 사용
5. 클래스는 구조 기반 타입을 지원한다.
- 타입스크립트는 클래스를 비교할 때 이름이 아니라 구조를 기준으로 삼는다.
- 즉, 클래스는 같은 프로퍼티와 메서드를 정의하는 기존의 일반 객체를 포함해 클래스 형태를 공유하는 다른 모든 타입을 할당할 수 있다.
- 하지만 protected나 private 필드를 갖는 클래스의 경우, 해당 클래스와 서브클래스의 인스턴스만 할당할 수 있다.
class First {
private x = 1
}
class Second extends First {
}
function f(a: First){}
f(new First) // OK !
f(new Second) // OK !
f({x : 1}) // First 타입에서 x는 private이지만 여기서는 아님.
6. 클래스는 값과 타입을 모두 선언한다.
- 값과 타입은 타입스크립트에서 다른 네임스페이스에 존재한다. 일반적으로 아래와 같이 타입스크립트가 알아서 값이나 타입으로 추론한다.
// 값
let a = 1000;
function b() {}
// 타입
type c = number;
interface d {}
- 클래스와 열거형은 타입 네임스페이스에 타입을, 값 네임스페이스에 값을 동시에 생성한다는 점에서 특별하다.
class C {}
let x: C = // 클래스의 인스턴스 타입
new C // 클래스 값
enum E { F, G }
let e: E = // 열거형 타입
E.F // 열거형 값
- 클래스와 열거형은 타입 수준에서 타입을 생성하기 때문에 'is-a' 관계를 쉽게 표현할 수 있다.
- is-a 관계는 'A(ex. 사과)는 B(ex.과일)이다' 처럼 카테고리 구별을 위한 관계이다. 이는 상속을 표현할 때 사용된다.
- 반면 'has-a' 관계는 'A는 B를 가지고 있다' 처럼 어떤 기능을 포함하는 관계이다. 클래스를 다른 클래스의 멤버 변수로 사용하는 경우이다.
- 클래스 선언 코드의 타입은 아래와 같이 인스턴스 타입과 생성자 타입을 합친 것이라고 볼 수 있다. 클래스 정의는 값 수준과 타입 수준에서 해석되고, 타입 수준에서는 두 가지로 타입으로 정의된다.
type State = {
[key: string]: string;
};
class StringDataBase {
state: State = {};
get(key: string): string | null {
return key in this.state ? this.state[key] : null;
}
set(key: string, value: string): void {
this.state[key] = value;
}
static from(state: State) {
let db = new StringDataBase();
for (let key in state) {
db.set(key, state[key]);
}
return db;
}
}
/**
* 클래스는 값 수준과 타입 수준 모두에서 정의된다.
* 클래스는 타입 수준에서 두 가지 타입을 생성한다.
* 1. 클래스의 인스턴스 타입
* 2. 클래스 생성자(constructor) 타입
*/
// 1. 클래스의 인스턴스 타입
interface StringDataBase {
state: State;
get(key: string): string | null;
set(key: string, value: string): void;
}
// 2. 클래스 생성자 타입
interface StringDatabasConstructor {
new (state: State): StringDataBase; // 생성자 시그니처 : new 연산자로 해당 타입을 인스턴스화 할 수 있음을 정의
setFlagsFromString(state: State): StringDataBase;
}
'FrontEnd > TypeScript' 카테고리의 다른 글
[TypeScript] 비동기 프로그래밍, 동시성과 병렬성 (0) | 2023.06.07 |
---|---|
[TypeScript] 에러 처리 (0) | 2023.06.05 |
[TypeScript] 가변성(슈퍼타입/서브타입 파악하기) & 할당성 & 타입 넓히기 (0) | 2023.06.03 |
[TypeScript] 함수의 타입 (0) | 2023.06.03 |
[TypeScript/타입스크립트 프로그래밍] 타입스크립트 타입 선언 & 종류 (2) | 2023.05.25 |