본문으로 건너뛰기

클래스에 타입 붙이기

타입스크립트 목차클래스레슨 41 / 51

목차

타입스크립트에서 클래스를 쓸 때는 인스턴스가 가질 속성 이름과 타입을 클래스 본문 맨 위에 먼저 선언하고, constructor나 기본값으로 초기값을 채웁니다. 자바스크립트처럼 constructor 안에서만 this.owner = owner라고 적으면 검사기는 그 속성이 클래스에 있다고 인정하지 않고(TS2339), 반대로 선언만 해 두고 초기값을 넣지 않으면 실행 때 undefined가 섞이지 않도록 TS2564 오류로 막아 줍니다.

카페 단골 도장 카드(StampCard)를 클래스로 만들면서 속성 선언, readonly 보호, 메서드 타입, 그리고 클래스 이름이 타입 자리와 값 자리 두 군데에 다 쓰이는 원리를 차례대로 살펴봅니다.

JS 다시 보기: 클래스와 new, 메서드와 this에서 class, constructor, this로 인스턴스를 만들고 메서드를 부르는 법을 다뤘습니다.

이 단원을 이미 안다면 단원 연습 문제의 진단 문항부터 풀고 틀린 레슨만 보세요.

6단계 중 1단계. 1~3번 줄: 클래스 본문 맨 위에 인스턴스가 가질 속성 이름(owner · stamps)과 타입을 먼저 적습니다.

1속성은 본문 맨 위에 선언하고 constructor에서 채운다

자바스크립트에서는 클래스 본문 맨 위에 아무것도 적지 않고 constructor 안에서 바로 this.owner = owner를 대입해도 동작했습니다. 타입스크립트에서는 클래스 본문 맨 위에 owner: string;처럼 속성 이름과 타입을 먼저 적어야 합니다.

값을 바꿀 일이 없는 손님 이름은 앞에 readonly를 붙여 readonly owner: string;⁠으로 선언하고, 처음에 0에서 시작하는 도장 개수는 stamps: number = 0;처럼 기본값을 바로 적어 둘 수 있습니다.

class StampCard {
  readonly owner: string;
  stamps: number = 0;

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

  add(n: number): number {
    this.stamps += n;
    return this.stamps;
  }
}

이어서 new StampCard("민지")로 인스턴스를 만들고 도장을 찍어 봅니다.

const card: StampCard =
  new StampCard("민지");
console.log(card.owner);
console.log(card.add(3));
실행 결과
민지
3

위 코드의 const card: StampCard⁠와 new StampCard("민지")에는 StampCard가 두 번 나옵니다.

  • : StampCard(타입 자리): 변수 card에 들어갈 인스턴스의 모양(owner: string, stamps: number, add: (n: number) => number)⁠을 가리킵니다.
  • new StampCard("민지")(값 자리): 실행 때 인스턴스를 찍어 내는 생성자 함수를 가리킵니다.

interface나 type 별칭은 타입 자리에만 놓을 수 있고 지우기 단계에서 사라지지만, class 선언은 타입 자리와 실행 값 자리를 한꺼번에 만듭니다.

2초기값을 빼먹으면 TS2564로 막는다

만약 속성을 stamps: number;라고 선언만 해 두고, 기본값(= 0)도 안 적고 constructor에서도 값을 넣지 않으면 어떻게 될까요? 실행할 때 this.stamps가 undefined가 되어 this.stamps += n이 NaN을 내놓게 됩니다.

타입스크립트의 엄격한 속성 초기화 검사(strictPropertyInitialization)⁠는 이 실수를 실행 전에 TS2564로 잡아냅니다.

class StampCard {
  readonly owner: string;
  stamps: number;

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

  add(n: number): number {
    this.stamps += n;
    return this.stamps;
  }
}
검사 결과 오류 1개
main.ts(3,3): error TS2564: Property 'stamps' has no initializer and is not definitely assigned in the constructor.

해결하는 길은 두 가지입니다.

  1. 선언 줄에 stamps: number = 0;처럼 기본값을 적는다.
  2. constructor 안에서 this.stamps = 0;처럼 빠짐없이 대입한다.

또한 readonly로 선언한 속성은 선언 줄이나 constructor 안에서만 값을 넣을 수 있습니다. 인스턴스를 만든 뒤 밖에서 바꾸려 하면 TS2540으로 막힙니다.

class StampCard {
  readonly owner: string;

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

const card =
  new StampCard("민지");
card.owner = "준호";
검사 결과 오류 1개
main.ts(11,6): error TS2540: Cannot assign to 'owner' because it is a read-only property.

예제

실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.

번호표 카운터(Counter) 클래스에 기본값 count: number = 0을 두고 inc를 두 번 부른 뒤 reset을 부릅니다.

class Counter {
  count: number = 0;

  inc(): number {
    this.count += 1;
    return this.count;
  }

  reset(): void {
    this.count = 0;
  }
}
const c = new Counter();
console.log(c.inc());
console.log(c.inc());
c.reset();
console.log(c.count);

무엇이 차례로 찍힐까요?

실행 결과 보기
실행 결과
1
2
0

count: number = 0;으로 기본값을 주면 constructor를 따로 적지 않아도 TS2564 없이 0으로 시작합니다. inc()는 계산한 number를 돌려주고, reset()은 돌려주는 값이 없으므로 반환 타입을 void로 적습니다.

자주 틀리는 곳

틀린 생각: 클래스 본문 맨 위 선언 없이 constructor 안에서만 this.owner = owner를 대입해도 된다.

자바스크립트 습관대로 클래스 본문 맨 위에 속성을 선언하지 않고 constructor 안에서 바로 this.owner = owner를 쓰면, 검사기는 StampCard에 owner라는 속성이 없다고 보고 TS2339를 냅니다.

class StampCard {
  constructor(owner: string) {
    this.owner = owner;
  }
}
검사 결과 오류 1개
main.ts(3,10): error TS2339: Property 'owner' does not exist on type 'StampCard'.

맞는 규칙: constructor 안에서 쓸 모든 this.속성은 먼저 클래스 본문 맨 위에 owner: string;처럼 선언해 두어야 합니다.

틀린 생각: constructor 안에 public owner: string처럼 매개변수 속성을 쓰면 어디서나 편하게 돌아간다.

오래된 타입스크립트 코드에서는 constructor의 매개변수 앞에 public이나 private을 붙여(public owner: string) 속성 선언과 this.owner = owner 대입을 한 줄로 줄여 쓰기도 했습니다. 하지만 이 문법은 타입만 지워서는 실행할 수 없고 생성자 본문에 this.owner = owner 코드를 새로 만들어 넣어야 하는 문법입니다.

class StampCard {
  constructor(
    public owner: string,
  ) {}
}
검사 결과 오류 1개
main.ts(3,5): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
실행 결과
main.ts:3  constructor(    public owner: string,           ^^^^^^^^^^^^^  ) {} SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode {  code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'}

맞는 규칙: 이 과목의 설정(erasableSyntaxOnly)과 Node 직접 실행에서는 매개변수 속성이 막히므로, 속성 선언(owner: string;)과 constructor 안의 대입(this.owner = owner;)은 항상 따로 적습니다.

틀린 생각: : StampCard 타입 자리에는 꼭 new StampCard로 만든 인스턴스만 들어갈 수 있다.

타입스크립트는 이름표가 아니라 모양을 봅니다(검사 규칙 4 — 구조적 타이핑).

class StampCard {
  owner: string;
  stamps: number = 0;
  constructor(owner: string) {
    this.owner = owner;
  }
}

const fake: StampCard = {
  owner: "지우",
  stamps: 5,
};
console.log(fake.owner);
실행 결과
지우

맞는 규칙: : StampCard 타입 자리에도 공개 속성(owner, stamps)의 모양만 똑같이 갖춘 일반 객체를 넣으면 검사를 통과합니다(검사 규칙 4).

확인 문제

1. 클래스 본문 맨 위에 stamps: number;만 적고 기본값도 constructor 대입도 생략하면 어떤 오류가 날까요?

힌트

2절에서 초기값을 빼먹었을 때 실행 전 어떤 오류 코드로 막았는지 떠올려 보세요.

정답

TS2564 오류가 납니다. 실행 때 this.stamps가 undefined로 남아 숫자 계산이 깨지는 일을 막기 위해 선언 줄이나 constructor에서 반드시 초기값을 넣도록 요구합니다.

2. constructor 매개변수 앞에 public owner: string을 붙이면 왜 이 과목 환경에서 막힐까요?

힌트

Node가 타입 표기를 어떻게 지우고 실행하는지(erasableSyntaxOnly) 생각해 보세요.

정답

타입만 지워서는 실행할 수 없는 문법이기 때문입니다. public owner를 지우는 것에 그치지 않고 this.owner = owner 대입문을 새로 만들어 넣어야 하므로, erasableSyntaxOnly에서는 TS1294가 나고 Node 직접 실행에서도 문법 오류가 납니다.

3. 변수 타입 표기 : StampCard와 생성자 호출 new StampCard("민지")에서 두 StampCard는 각각 무슨 뜻인가요?

힌트

1절에서 class 선언이 동시에 만드는 두 자리를 살펴보세요.

정답

앞의 : StampCard는 인스턴스의 모양을 나타내는 타입이고, 뒤의 new StampCard는 인스턴스를 만들어 내는 생성자 함수(값)입니다. class 선언은 이 두 자리를 동시에 만듭니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 1절의 StampCard 코드를 main.ts로 저장하고 stamps: number = 0;에서 = 0을 지운 뒤 npx tsc로 TS2564가 나는지 직접 확인해 보세요. 다음 레슨 private과 #에서는 클래스 속성을 밖에서 숨기는 두 가지 방법을 비교합니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

타입스크립트 목차로