[TS] 인터페이스

ClassBinu·2024년 3월 21일

인터페이스

일반적으로 객체 타입에 사용함.
(확장이 더 쉬움.)

기본 인터페이스

interface Person {
  name: string;
  age: number;
}

// ✅
const person: Person = {
  name: "Mark",
  age: 39,
};

// ❌: 프로퍼티가 없음
const person2: Person = {
  name: "Anna",
};

// ❌: 불필요한 프로퍼티
const person3: Person = {
  name: "Anna",
  age: 39,
  language: "ko",
};

선택적 프로퍼티

interface Account {
  bank: string;
  name: string;
  age: number;
  memo?: string;
}

// ✅: memo는 없어도 됨
const account: Account = {
  bank: "kakao",
  name: "Mark",
  age: 39,
};

// ✅: memo가 있어도 됨
const account2: Account = {
  bank: "kakao",
  name: "Mark",
  age: 39,
  memo: "test",
};

읽기 전용 프로퍼티

interface Point {
  readonly x: number;
  readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // 오류!

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // 오류!
ro.push(5); // 오류!
ro.length = 100; // 오류!
a = ro; // 오류!

// 단, 타입 단언으로 오버라이딩 가능
a = ro as number[];

readonly vs const

readonly는 프로퍼티
const는 변수

초과 프로퍼티 검사

기본적으로 명시되지 않은(초과된) 프로퍼티 사용 시 오류가 발생함.
이 검사를 피할 수 있는 방법이 있지만 오류를 우회하는 것보다 타입 정의를 수정하는 게 좋음.

  • 타입 단언
  • 문자열 인덱스
  • 변수 할당

함수 타입

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  return source.search(subString) > -1;
};

함수 타입에서 매개변수 이름은 상관 없음. 위치를 기준으로 타입을 체크함.

인덱서블 타입

객체가 인덱스를 통해 접근될 때 어떤 타입 값을 반환할지 지정

interface StringArray {
  [index: number]: string; // 인덱스(숫자)로 접근했을 때 문자열을 반환한다.
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

읽기 전용 인덱스 시그니처

interface ReadonlyStringArray {
    readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // 오류: 인덱스 시그니처가 읽기 전용입니다.

배열은 const로 선언해도 내부의 값은 바꿀 수 있지만 읽기 전용 인덱스 시그니처를 통해 읽기 전용으로 바꿀 수 있음.

클래스타입

인터페이스로 클래스의 내용을 강제할 수 있음.

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date): void;
}

class Clock implements ClockInterface {
    currentTime: Date = new Date();
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

인터페이스 vs abstract class

인터페이스는 시그니처(계약)만 정의한다. 구현 내용이 표함되지 않는다.
추상 클래스는 실제 구현 코드 포함할 수 있다.

인터페이스는 다중 상속 허용하여(implements),
추상 클래스는 단일 상속만 가능하다.(extends)

인터페이스는 타입 체킹, 계약 정의가 초점이며,
추상 클래스는 코드 재사용과 공통 기능 구현에 사용된다.

클래스 스태틱

클래스는 두 가지 타입을 가진다.

  • 스태틱 타입
  • 인스턴스 타입

클래스가 인터페이스를 implements 하면 클래스의 인스턴스만 검사한다.
constructor 생성자는 스태틱이므로 검사에 포함되지 않는다.

그래서 이런 오류가 발생함.

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor { // 오류: 클래스 'Clock'가 인터페이스 'ClockConstructor'를 올바르게 구현하지 못함.
    currentTime: Date;
    constructor(h: number, m: number) { }
}

그래서 생성자 함수를 수동(?)으로 구현하는 식으로 처리

interface ClockConstructor {
  new (hour: number, minute: number);
}

class Clock implements ClockConstructor { // constructor() 때문에 에러 발생함.
  currentTime: Date;
  constructor(h: number, m: number) {}
}

function createClock(
  ctor: ClockConstructor,
  hour: number,
  minute: number
): Clock {
  return new ctor(hour, minute);
}

이런걸 팩토리 함수라고 한다?!

인터페이스는 확장 가능

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

하이브리스 타입

하나의 변수가 함수처럼 호출될 수도 있고, 객체(프로퍼티 + 메서드) 처럼 동작 가능

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = function (start: number) {} as Counter;
  counter.interval = 123;
  counter.reset = function () {
    this.interval = 0;
  };
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

클래스를 확장한 인터페이스

인터페이스가 클래스의 "계획도"를 빌려가는 것과 비슷
계획도에는 클래스의 필드, 메서드 등 세부정보가 있지만, 어떻게 구현해야 하는지는 포함되지 않음.

상속 vs 확장

상속: 클래스가 클래스를 상속받을 때는 모든 속성과 메서드(구현을 포함하여) 상속
확장: 인터페이스가 클래스를 확장할 때는, 클래스의 멤버들의 '리스트'만 상속받고, 그 구현을 상속받지 않음.

// 'Control' 클래스 정의: 모든 컨트롤의 기본이 되는 클래스.
// 이 클래스에는 외부에서 접근할 수 없는 'state'라는 private 멤버 변수가 있습니다.
class Control {
  private state: any;
}

// 'SelectableControl' 인터페이스 정의: 'Control' 클래스를 확장하고,
// 'select' 메서드를 추가하는 인터페이스.
// 이 인터페이스는 'Control'의 모든 멤버를 상속받지만 구현은 상속받지 않습니다.
// 'select' 메서드는 구현되어야 합니다.
interface SelectableControl extends Control {
  select(): void;
}

// 'Button' 클래스 정의: 'Control' 클래스를 상속받고,
// 'SelectableControl' 인터페이스를 구현합니다.
// 'select' 메서드를 구현하여 'SelectableControl' 인터페이스의 요구 사항을 충족시킵니다.
class Button extends Control implements SelectableControl {
  select() {}
}

// 'TextBox' 클래스 정의: 'Control' 클래스를 상속받습니다.
// 이 클래스는 'select' 메서드를 가지고 있지만, 'SelectableControl' 인터페이스를 명시적으로 구현하진 않습니다.
// 그러나 'select' 메서드의 존재로 인해 'SelectableControl'의 형태를 갖추고 있습니다.
class TextBox extends Control {
  select() {}
}

인터페이스는 심지어 기초 클래스의 private과 protected 멤버도 상속받습니다.

0개의 댓글