[TypeScript] Interface

HANJIN·2019년 12월 24일
5

TypeScript

목록 보기
2/3
post-thumbnail

Intro

TypeScript의 핵심 원리 중 하나는 값이 가지는 형태에 초점을 맞추는 타입체킹을 한다는 것입니다.
이것은 "덕 타이핑(duck typing)" 또는 "구조적 서브타이핑(structural subtyping)"이라고도 합니다.

덕 타이핑
객체의 변수 및 메소드의 집합이 객체의 타입을 결정하는 것을 말한다.
클래스 상속이나 인터페이스 구현으로 타입을 구분하는 대신,
덕 타이핑은 객체가 어떤 타입에 걸맞는 변수와 메소드를 지니면 해당 타입에 속하는 것으로 간주한다.
"덕 타이핑" 이라는 용어는 "덕 테스트" 에서 유래했다.
만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.

우선 인터페이스에 대해 간단히 배워 본 다음, 위에서 말한 것들의 의미를 다시 얘기하도록 하겠습니다.

Interface

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

위 코드에서 타입체커는 printLabel에 대한 호출을 확인합니다.
printLabel 함수는 String 타입의 값을 가지는 label이란 프로퍼티를 가진 객체를 인자로 받습니다.

컴파일러는 '최소한' 요구 조건에 맞는지 체크합니다.
즉, printLabel 함수의 인자는 객체이며 '최소한' String 타입의 값을 가지는 label이란 프로퍼티를 가진다.
라는 의미입니다.

예를 들어, 위 코드에서 printLabel함수는 myObj라는 객체를 인자로 받으며 호출되었습니다.
myObj 객체는 Number 타입의 값을 가지는 size 프로퍼티, 그리고 String 타입의 값을 가지는 label 프로퍼티로 이루어져있습니다.

label 프로퍼티가 존재하고 String 타입의 값을 가졌기 때문에, 객체의 다른 키와 밸류 값에 상관없이 조건을 만족합니다.

위의 코드는 아래와 같이 재작성할 수 있습니다.

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

이런 식으로 printLabel이 객체로 받는 인자에 필요한 프로퍼티들을 모아서 하나의 이름으로 선언할 수 있습니다.

프로퍼티는 순서에 관계가 없으며 필요한 속성과 타입만을 체크합니다.

Optional Properties

인터페이스에는, 경우에 따라 프로퍼티가 필요할 수도 아닐수도 있습니다.
이 경우에, Optional 한 프로퍼티를 선언하여 해결할 수 있습니다.

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({ color: "black" });

Readonly Properties

읽기전용 프로퍼티를 선언할 수 있습니다.

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

let p1: Point = { x: 10, y: 20};
p1.x = 5; // 에러

Readonly Array

읽기전용 배열을 선언할 수 있습니다.

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; 읽기전용 배열을 일반 배열로 재할당하는 것 조차도 금지됩니다.
하지만 type assertion을 통해 오버라이드 할 수 있습니다.

a = ro as number[];

readonly vs const

변수는 const, 프로퍼티는 readonly를 사용합니다.

Excess Property Checks

인자의 타입으로 지정 된 인터페이스에 없는 프로퍼티가 있을 경우 오류가 발생합니다.

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  // ...
}

// colour은 SquareConfig 타입에 필요하지 않습니다.
let mySquare = createSquare({ colour: "red", width: 100 });

검사하는 가장 쉬운 방법은 type assertion을 사용하는 것 입니다.

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

SquarConfig가 추가로 프로퍼티를 가지는 경우 다음과 같이 정의할 수 있습니다.

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;

이렇게 작성을 했을 경우, color, width가 아닌 프로퍼티들의 타입은 상관이 없습니다.

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

위와 같은 방식으로 작성을 하였을 때, 프로퍼티 초과 검사를 하지 않으므로 오류가 발생하지 않습니다만, 이러한 검사를 '회피하는' 코딩은 하지 말아야 합니다.

Functin Types

인터페이스는 프로퍼티를 가진 객체 이외에, 함수 타입을 선언하는데 이용될 수도 있습니다.

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

매개 변수 목록과, 반환 타입만 주어진 함수 선언과 같은 형태로 작성합니다.

아래와 같은 형태로 함수 인터페이스를 사용합니다.

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

매개 변수의 이름이 일치할 필요는 없습니다.

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};

리턴 값이 숫자 혹은 문자열 등 다른 타입인 경우 경고가 출력됩니다.

Indexable Types

인터페이스는 a[10] 또는 ageMap["daniel"] 처럼 '인덱스'를 생성할 수 있는 타입을 만들 수도 있습니다.

interface StringArray {
  [index: number]: string; // index signature
}

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

let myStr: string = myArray[0];

위의 인덱스 시그니처는 StringArray는 number로 인덱싱 될 때 string을 반환한다는 의미입니다.

타입스크립트에서 지원되는 인덱스 시그니처에는 문자열과 숫자 두 종류가 있습니다.

두 가지 모두를 지원하지만,
숫자 인덱서의 반환 타입은, 문자열 인덱서의 반환 타입의 하위 타입이어야합니다.

설명이 좀 어렵네요. 코드로 한번 보도록 할게요.

class Animal {
  name: string;
}
class Dog extends Animal {
  breed: string;
}

// 오류: numeric과 string으로 인덱싱하면 완전히 다른 타입의 Animal을 얻을 수 있습니다!
interface NotOkay {
  [x: number]: Animal;
  [x: string]: Dog;
}

number로 인덱싱이 지원된다 하더라도 결국에 내부적으로 a[100]은 a['100']과 동일하므로 더 넓은 범위를 커버하는 스트링이 반환값에서도 넓은 범위의 조건이 되어야 한다는 것입니다.

Animal이 Dog에게 상속하므로, Animal이 더 상위 타입이라고 판단할 수 있고 string인덱서의 반환 타입이 Dog인 것이 조건에 부합하지 않기 때문에 에러를 발생시키게 됩니다.

문자열 인덱스 시그니처는 모든 프로퍼티가 반환타입과 일치하도록 강제합니다.
obj.property는 obj["property"]와 같은 의미로 사용되기 때문입니다.

즉, 아래와 같은 코드에서는 오류를 발생시키게됩니다(모든 프로퍼티의 타입은 시그니처의 반환타입 같은 타입이 되어야함).

interface NumberDictionary {
  [index: string]: number;
  length: number; // 좋아요, length는 number입니다.
  name: string; // 오류, 'name'의 타입이 인덱서의 하위 타입이 아닙니다.
}

readonly를 사용하여 인덱스 시그니처를 읽기 전용으로 만들 수 있습니다.

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // 오류!

배열과 비슷하게 보이지만, 배열에서 사용가능한 어떤 메소드도 적용할 수 없습니다.

Class Types

C#, Java 등의 언어에서 인터페이스의 가장 일반적인 용도는 클래스 제약조건 명시입니다.

interface ClockInterface {
  currenrTime: Date;
}

class Clock implements ClockInterface {
  currentTime: Date;
  constructor(h: number, m: number) {}
}

또한, 메서드의 매개변수, 반환타입 등도 명시 할 수 있습니다.

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

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

위의 코드는 interface의 setTime선언 부분에서 경고를 출력합니다(출처는 공식문서...).

메서드의 반환타입이 명시되어있지 않기때문입니다. 묵시적으로 any로 지정되지만, 타입스크립트를 사용하는 이상 타입을 지정해주는 것이 더 좋다고 판단됩니다.

인터페이스는 public, private 양쪽이아닌 public 클래스만 만듭니다.
클래스를 사용하여 클래스 인스턴스의 private측에 특정 타입이 있는지 검사하는 것은 금지되어 있습니다.

Difference between the static and instance sides of classes

클래스와 인터페이스로 작업할 때 클래스에 두 가지 타입이 있음을 기억해야합니다.
'static, instance 타입'
construct signature 로 인터페이스를 만들고 이 인터페이스를 구현하는 클래스를 생성하려고 하면 오류가 발생할 수 있습니다.

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

class Clock implements ClockConstructor {
  currentTime: Date;
  constructor(h: number, m: number) {}
}

클래스가 인터페이스를 구현할 때, 클래스의 인스턴스 측면만 검사되기 때문입니다.
생성자는 정적인 측면에 속합니다.

이 문제를 해결하려면 클래스의 정적인 측면에서 검사해야합니다.

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

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

class DigitalClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
}
class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("tick tock");
  }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

일반적인 클래스를 통한 인터페이스 구현과 반대로 이루어집니다.

아래 코드는 흔히 사용하는 클래스를 통한 인터페이스 구현의 예제입니다.

interface I_FileManager {
  send(fileName: string);
  receive(fileName: string);
}

class FileManager implements I_FileManger{
  contructor(){}
  send(fileName: string){
    // ...
  }
  receive(fileName: string){
    // ...
  }
}

const fileManager = new FileManager();
fileManager.send('1.jpg'); 

Extending Interfaces

클래스처럼 인터페이스도 확장이 가능합니다.
예제로 바로 보시면 이해가 빠르실겁니다.

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = {} as Square; // <Square>{}; 같은 표현입니다.
square.color = "blue";

여러 인터페이스를 결합할 수 있습니다.

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

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

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

Hybrid Types

메서드와 프로퍼티 모두를 가진 하이브리드 타입의 인터페이스도 있습니다.

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;

Interfaces Extending Classes

인터페이스가 클래스를 확장하면 멤버를 상속하지만 구현은 상속하지 않습니다. 이 패턴은 private 및 protected 멤버도 상속합니다.

private 또는 protected 멤버가 있는 클래스를 확장하는 인터페이스는 해당 클래스 또는 해당 클래스의 서브 클래스에서만 구현 가능합니다.

예시)

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button extends Control implements SelectableControl {
  select() {}
}

class TextBox extends Control {
  select() {}
}

// 오류: 'Image' 타입의 'state' 프로퍼티가 없습니다.
class Image implements SelectableControl {
  select() {}
}

class Location {}

위 코드에서 SelectableControl에는 private state 프로퍼티를 포함한 Control의 모든 멤버가 포함되어있습니다.

state는 private 멤버이기 때문에 Control의 자식만 SelectableControl을 구현할 수 있습니다.

private 멤버는 해당 클래스, 혹은 해당 클래스의 서브 클래스에만 존재합니다.

SelectableControlselect메서드를 가진 Control과 같은 역할을 합니다.
ButtonTextBox클래스는 둘다 Control을 상속받고 select메서드를 가졌기 때문에 SelectableControl의 하위 타입입니다.

Image는 Control을 상속하지 않았기 때문에 state 프로퍼티가 존재하지 않습니다.

profile
소프트웨어 엔지니어.

0개의 댓글