본문 바로가기
TypeScript

[Typescript] 2. ts-for-jsdev 인터페이스와 클래스 공부

by NJ94 2023. 9. 30.

인터페이스와 클래스

  • 인터페이스 기초
  • 색인 기능 타입
  • 인터페이스 확장
  • 클래스
  • 클래스 확장
  • 클래스 심화
  • 인터페이스와 클래스의 관계

 

interface

사용자 정의 타입을 정의하는 방법중 하나. 인터페이스는 객체의 구조를 정의하며, 해당 객체가 반드시 가져야하는 멤버와 해당 멤버의 데이터타입을 지정한다.

 

class

객체 지향 프로그래밍의 핵심 개념중 하나로, 객체를 생성이 가능하다. 클래스는 속성(멤버 변수)와 메서드(멤버 함수)를 가질 수 있으며, 객체를 생성하고 초기화하는데 사용된다. 

 

interface


interface User {
  name: string;
  height: number;
}

 

읽기 전용 속성, 선택 속성 정의

interface User {
  name: string;
  readonly height: number;
  favoriteLanguage?: string;
}
const author: User = { name: '안희종', height: 176 }; // ok
author.height = 183; // error TS2540: Cannot assign to 'height' because it is a constant or a read-only property.

함수 인터페이스

interface GetUserName {
  (user: User): string;
}
const getUserName: GetUserName = function (user) {
  return user.name;
};

hybrid type

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

제네릭 인터페이스

interface MyResponse<Data> {
  data: Data;
  status: number;
  ok: boolean;
  /* ... */
}
inteface User {
  name: string;
  readonly height: number;
  /* ... */
}
const user: MyReponse<User> = await getUserApiCall(userId);
user.name; // 타입 시스템은 user.name이 string임을 알 수 있다.

type, interface의 차이

type과 interface는 비슷한 점이 많지만, 두 개념 사이엔 다음과 같은 차이점이 있다.

 

- type을 이용해서 기본 타입, 배열, 튜플, 유니온 타입 등에 새로운 이름을 붙일 수 있다.

인터페이스는 해당 타입을 표현하는 것이 불가능하다.

ex) type UUID = string

 

- type은 실제로 새 타입을 생성하지 않는다. 따라서 type User = { name: string; } 타입과 관련된 타입 에러가 발생했을 시 에러 메세지는 User 대신 { name: string; }를 보여준다. 인터페이스는 실제로 새 타입을 생성하고, interface User { name: string; } 과 관련된 에러 메세지는 User 가 등장한다.

 

- interface는 extends 키워들 이용해 확장할 수 있지만, type은 불가능하다.

 

기본적으로 인터페이스로 표현할 수 있는 모든 타입은 인터페이스로 표현하고, 기본 타입에 새로운 이름ㅇ르 붙이고 싶거나 유니온 타입을 명명하고 싶은 경우 등 인터페이스의 능력 밖인 부분은 type을 사용하는게 맞다.

 

색인 가능 타입 Indexable type


코드의 실행 시점에서만 알 수 있는 이름의 동적 속성을 갖는 타입.

색인 시그니쳐

색인 기능 타입을 이용해 indexable 객체의 타입을 정의할 수 있다. 색인 기능 타입을 정의하기 위해서는 사용하는 기 호인 대괄호 [] 를 이용해 객체의 색인 시그니쳐를 적어 줘야한다.

interface NameHeightMap {
  [userName: string]: number | undefined;
}

 

 

- 임의의 string 타입 값 userName 으로 색인한 값 [userName: string] -> 인덱스 시그니쳐

- 즉 nameHeightMap[userName]은 number 또는 undefined 타입의 값이다. ( : number | undefined )

색인과 타입

색인 타입은 문자열 또는 숫자만 사용 가능하며, 문자열 색인과 숫자 색인이 모두 존재하는 경우, 숫자로 색인 된 값의 타입은 문자열로 색인된 값 타입의 서브타입이어야 한다.

inteface Mixed<A, B> {
  [stringIndex: string]: A;
  [numberIndex: number]: B;
}

자바스크립트 색인의 동작방식은 자바스크립트 코드에서 객체의 색인에 접근할 때, 내부적으로 색인의 toString() 메소드를 호출해 문자열로 변형된 값을 색인으로 사용한다. 1.toString() === '1' 이므로 obj[1] 이라고 적은 코드는 실제로 obj['1'] 와 동일하다.

interface ErrorProne {
  [str: string]: number;
  [num: number]: boolean;
}
let errorProne: ErrorProne = {
  'abc': 3,
  3: true
};
errorProne[3];

errorPhone[3]; 의 3은 숫자 타입이므로 타입 시스템은 errorPhone[3]의 타입이 boolean 일 것이라고 추측할 것 이다.

하지만 위에서 언급한 색인의 동작 방식에 의해 실제로 해당 값은 errorPhone['3'] 과 같고, 이는 문자열 색인으로 접근한 number 타입의 값이다. 타입 시스템이 알고 있는 정보 boolean와 실제 상황 number 이 달라지는 것이다.

 

따라서 타입스크립트는 이런 코드를 작성하는 것을 허용하지 않고,  error TS2413: Numeric index type 'boolean' is not assignable to string index type 'number'. 와 같은 에러를 발생시킨다. 숫자 색인으로 접근한 타입 boolean을 문자열 색인으로 접근한 타입 number 에 할당할 수 없다는 의미다.

읽기 전용 색인

interface ReadonlyNameHeightMap {
  readonly [name: string]: height;
}
const m: ReadonlyNameHeightMap = { '안희종': 176 };
m['안희종'] = 177; // error TS2542: Index signature in type 'ReadonlyNameHeightMap' only permits reading.

색인 기능 타입의 사용 예

만약 색인 기능 타입 없이 T 타입의 원소를 갖는 Array를 정의할경우 아래와 같이 일일이 정의해야한다.

interface Array<T> {
  length: number;
  0?: T;
  1?: T;
  /* ... */
  Number.MAX_SAFE_INTEGER?: T;
  /* 메소드 정의 */
}

인덱스 타입을 이용하면 간결하게 대체 가능

interface Array<T> {
  length: number;
  [index: number]?: T;
  /* 메소드 정의 */
}

 

인터페이스 확장


interface User {
  name: string;
  readonly height: number;
  favoriteLanguage?: string;
}

extends 키워드를 이용한 인터페이스 확장을 통해 기존 인터페이스를 효율적으로 재사용 할 수 있다.

 

interface LoggedInUser extends User {
  loggedInAt: Date;
}

다수의 인터페이스 동시 확장

interface ElectricDevice {
  voltage: number;
}
interface SquareShape {
  width: number;
  height: number;
}
interface Laptop extends ElectricDevice, SquareShape {
  color: string;
}
const macbook15: Laptop = { voltage: 220, width: 30, height: 21; color: 'white' };

 

클래스


ES6에는 기존의 객체 지향 언어와 비슷하게 클래스를 선언할 수 있는 class 키워드가 추가되었다.

TS 클래스는 ES6 클래스의 상위 집합으로, ES6 클래스를 포함할 뿐 아니라 여러 추가 기능을 제공한다.

class NothingImportant {}

생성자

constructor 키워드를 사용해 클래스 생성자 class constructor 를 정의할 수 있다.

class Dog {
  constructor() {
    console.log('constructing!');
  }
}
const dog: Dog = new Dog(); // constructing!
class BarkingDog {
  constructor(barkingSound: string) {
    console.log(`${barkingSound}!`);
  }
}

const barkingDog: BarkingDog = new BarkingDog('월'); // 월!

속성

객체 속성과 유사하게 클래스 인스턴스도 속성 property를 가질 수 있다. 클래스 내에서는 속성엔 this 키워드를 이용해 접근 가능하다. 

class Triangle {
  // 속성 선언
  vertices: number;
  constructor() {
    // 속성 할당
    this.vertices = 3;
  }
}
const triangle: Triangle = new Triangle();
console.log(triangle.vertices); // 3

속성 기본값

class Triangle {
  vertices: number = 3;
}
const triangle: Triangle = new Triangle();
console.log(triangle.vertices); // 3

읽기 전용 속성

class Triangle {
  readonly vertices: number;
  constructor() {
    this.vertices = 3;
  }
}
const triangle: Triangle = new Triangle();
triangle.vertices = 4;
// error TS2540: Cannot assign to 'vertices' because it is a constant or a read-only property.

메소드

class BarkingDog {
  barkingSound: string;
  
  constructor(barkingSound: string) {
    this.barkingSound = barkingSound;
  }
  
  bark(): void {
    console.log(`${this.barkingSound}!`);
  }
}
const barkingDog: BarkingDog = new BarkingDog('월');
barkingDog.bark(); // 월!

 

클래스 확장


class Base {
  answer: number = 42;
  greetings() {
    console.log('Hello, world!');
  }
}
class Extended extends Base { }
const extended: Extended = new Extended();
console.log(extended.answer); // 42
extended.greetings(); // Hello, world!

클래스 확장 시 생성자

슈퍼클래스의 생성자는 서브클래스의 생성자에서 자동 호출되지 않기 때문에, 서브 클래스의 생성자에서는 반드시 super 키워드를 사용해 슈퍼클래스의 생성자를 호출해줘야 한다.

class Base {
  baseProp: number;
  constructor() {
    this.baseProp = 123;
  }
}
class Extended extends Base {
  extendedProp: number;
  constructor() {
    super(); // 반드시 이 호출을 직접 해 주어야 한다.
    this.extendedProp = 456;
  }
}
const extended: Extended = new Extended();
console.log(extended.baseProp); // 123
console.log(extended.extendedProp); // 456

error TS2377: Constructors for derived classesmust contain a 'super' call.

 

 

class ExtendedWithoutSuper extends Base {
  constructor() { }
} 
// error TS2377: Constructors for derived classesmust contain a 'super' call.

 

클래스 심화

 

스태틱 멤버


클래스 전체에서 공유되는 값이 필요한 경우 스태틱 멤버 static member를 사용할 수 있다.

 

static 키워드는 클래스의 정적 멤버를 정의할 때 사용된다. 정적 멤버는 클래스 자체에 속하며, 정적 멤버는 클래스의 모든 인스턴스에서 공유되며, 주로 클래스 레벨의 동작이나 데이터를 표현하기 위해 사용된다.

 

스태틱 속성

속성 선언 앞에 static 키워드를 붙여 스태틱 속성 정의

class Counter {
  static count: number = 0;
}
console.log(Counter.count); // 0

스태틱 메소드

class Counter {
  static count: number = 0;
  static increaseCount() {
    Counter.count += 1;
  }
  static getCount() {
    return Counter.count;
  }
}
Counter.increaseCount();
console.log(Counter.getCount()); // 1
Counter.increaseCount();
console.log(Counter.getCount()); // 2

 

 

접근 제어자


public

접근 제한이 없으며, 프로그램의 어느 곳에서나 접근 가능

 

private

해당 클래스 내부의 코드만이 접근 가능하지만, 서브 클래스에서는 접근 불가

 

protected

private과 비슷하지만, 서브 클래스에서의 접근 또한 허용된다.

 

 

접근자 


접근 제어자를 사용해 직접적인 접근을 막고 읽기, 쓰기를 위한 메소드를 노출하는 것.

class Shape {
  private _vertices: number = 3;
  
  getVertices() {
    console.log('Vertices getter called.');
    return this._vertices;
  }
  
  setVertices(value) {
    console.log('Vertices setter called.');
    this._vertices = value;
  }
}

읽기 접근을 위한 게터

class Shape {
  constructor (public vertices: number) { }
  get vertices(): number {
    console.log('Vertices getter called.');
    return 3;
  }
}
const triangle: Shape = new Shape(3);
const vertices = triangle.vertices; // Vertices getter called.
console.log(vertices); // 3

쓰기 접근을 위한 세터

class Shape {
  private _vertices: number = 3;
  get vertices() {
    console.log('Vertices getter called.');
    return this._vertices;
  }
  set vertices(value) {
    console.log('Vertices setter called.');
    this._vertices = value;
  }
}
const square = new Shape();
square.vertices = 4; // Vertices setter called.
const vertices = square.vertices; // Vertices getter called.
console.log(vertices); // 4

 

추상클래스


class 키워드 대신 abstract class 키워드를 사용해 추상 클래스를 선언할 수 있다. 일반 클래스는 extends 키워드를 사용해 추상 클래스를 확장 할 수 있따. 추상 클래스는 인스턴스화가 불가능하다는 점에서 일반 클래스와 다르다. 또한 추상 클래스는 구현을 일부 포함할 수 있다는 점에서 인터페이스와 다르다

abstract class Animal {
    move(): void {
        console.log("roaming the earth...");
    }
    abstract makeSound(): void;
}

 

추상 클래스는 객체 지향 프로그래밍에서 사용되는 중요한 개념 중 하나로, 직접 인스턴스화(객체를 생성)할 수 없는 클래스입니다. 추상 클래스는 다른 클래스들에게 공통된 속성과 메서드를 제공하고, 이를 상속받는 하위 클래스들이 이를 구체화하도록 강제합니다. TypeScript에서도 추상 클래스를 지원하며, 클래스 선언 시 abstract 키워드를 사용하여 클래스를 추상 클래스로 만들 수 있습니다.

 

추상 클래스의 주요 특징과 사용법:

  1. 추상 클래스는 직접 인스턴스화할 수 없으므로 추상 클래스의 객체를 생성할 수 없습니다.
  2. 추상 클래스는 다른 클래스에서 상속받아야 합니다. 하위 클래스들은 추상 클래스에서 정의한 메서드를 반드시 구현해야 합니다.

 

참고


https://ahnheejong.gitbook.io/ts-for-jsdev/04-interface-and-class/intro

 

4.0 들어가며 - ts-for-jsdev

클래스(class)를 이용해 객체 지향 프로그래밍 언어와 비슷한 방식으로 코드를 구조화 할 수 있다. 타입스크립트의 클래스는 ES6에 추가된 클래스 문법의 확장으로, 접근 제어자 등의 유용한 추가

ahnheejong.gitbook.io