👶 TypeScript

클래스는 값과 타입을 모두 선언한다

개발자 린다씨 2023. 1. 16. 12:00
반응형

클래스는 값과 타입을 모두 선언한다

TypeScript의 거의 모든 것은 아니면 타입입니다.

// 값
let a = 1999
function b(){}

// 타입
type a = number
interface b {
  (): void
}

 값과 타입은 TypeScript에서 별도의 네임스페이스에 존재합니다.

 

용어를 어떻게 사용하는지를 보고 TypeScript가 알아서 이를 값 또는 타입으로 해석합니다.

// 값
let a = 1999
function b(){}

// 타입
type a = number
interface b {
  (): void
}

if(a + 1 > 3){ // 문맥상 값 a로 추론
  let x: a = 3 // 문맥상 타입 a로 추론
}

문맥을 파악해 해석하는 이 기능은 정말 유용합니다.

 

한편 클래스와 열거형은 타입 네임스페이스에 타입을, 값 네임스페이스에 값을 동시에 생성한다는 점에서 특별합니다.

class C {}
let c: C // ①
= new C // ②

enum E { F, G }
let e: E // ③
= E.F // ④
  1. 문맥상 C는 C 클래스의 인스턴스 타입을 가리킵니다.
  2. 문맥상 C는 C를 가리킵니다.
  3. 문맥상 E는 E 열거형의 타입을 가리킵니다.
  4. 문맥상 E는 값 E를 가리킵니다.

클래스를 다룰 때는 "이 변수는 이 클래스의 인스턴스여야 한다"라고 표현할 수 있는 방법이 필요한데, 이는 열거형도 마찬가지입니다.

 

클래스와 열거형은 타입 수준에서 타입을 생성하기 때문에 이런 'is-a'관계를 쉽게 표현할 수 있습니다.

 

런타임에 new로 클래스를 인스턴스화하거나, 클래스의 정적 메서드를 호출하거나, 메타 프로그래밍하거나, instanceof 연산을 수행하려면 클래스의 값이 필요합니다.

 

위의 예제에서 C는 C 클래스의 인스턴스를 가리켰습니다.

 

C 클래스 자체를 가리키려면 typeof 키워드를 사용하면 됩니다.

 

단순한 데이터베이스인 StringDatabases라는 클래스를 만들어보겠습니다.

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

StringDatabase의 인스턴스 타입은 아래와 같습니다.

interface StringDatabase {
  state: State
  get(key: string): string | null
  set(key: string, value: string): void
}

다음은 typeof StringDatabase의 생성자 타입입니다.

interface StringDatabaseConstructor {
  new(): StringDatabase
  from(state: State): StringDatabase
}

StringDatabaseConstructor는 .from이라는 한 개의 메서드를 포함하며 new는 StringDatabase 인스턴스를 반환합니다.

 

두 인터페이스를 합치면 StringDatabase 클래스의 생성자와 인스턴스가 완성됩니다.

 

new() 코드를 생성자 시그니처(constructor signature)라 부르며, 생성자 시그니처는 new 연산자로 해당 타입을 인스턴스화할 수 있음을 정의하는 TypeScript의 방식입니다.

 

TypeScript는 구조를 기반으로 타입을 구분하기 때문에 이 방식('클래스란 new로 인스턴스화할 수 있는 어떤 것')이 클래스가 무엇인지를 기술하는 최선입니다.

 

앞의 예는 인수를 전혀 받지 않는 생성자이지만 인수를 받는 생성자도 선언할 수 있습니다.

 

예를 들어 아래는 StringDatabase가 선택적으로 초기 상태를 받도록 수정한 모습입니다.

class StringDatabase {
    constructor(public state: State = {}){}
    // ...
}

이 StringDatabase의 생성자 시그니처는 다음과 같습니다.

class StringDatabase {
    constructor(public state: State = {}){}
    // ...
}

interface StringDatabaseConstructor {
    new(state?: State): StringDatabase
    from(state: State): StringDatabase
}

클래스 정의는 용어를 값 수준과 타입 수준으로 생성할 뿐 아니라, 타입 수준에선 두 개의 용어를 생성했습니다.

 

하나는 클래스의 인스턴스를 가리키며, 다른 하나는 (typeof 타입 연산자로 얻을 수 있는) 클래스 생성자 자체를 가리킵니다.

반응형