[TypeScript 기본 이론] #2 Interface

mechaniccoder·2020년 6월 30일
0

TypeScript 기본이론

목록 보기
2/6
post-thumbnail

소개


TypeScript의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞추고 있다는 거야. 이를 "duck typing" 또는 "structural subtyping" 이라고 해. 간단한 예시를 한번 보여줄게.

Interface


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

let myObj = { size: 10, label: "This is a string"};
printLabel(myObj);

실제로는 label 프로퍼티말고 다른 프로퍼티도 가지고 있지만 최소한의 조건을 만족하는지 컴파일러가 검사할거야. 이 예제를 interface api를 사용해서 똑같이 구현해보자.

interface LabeledValue {
  label: string;
}

function printLabel(labelObj: LabeledValue) {
  console.log(labelObj.label);
}

let myObj = { size: 10, label: 'this is a string'};
printLabel(myObj);

Optional properties


인터페이스의 모든 프로퍼티가 필요한게 아니라 선택적으로 조건을 주고 싶을 때는 ? 를 쓰면 돼.

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


일부 프로퍼티의 경우 처음 생성될 때만 수정하고 이후에 수정 불가능하게 하고 싶으면 readonly를 활용할 수 있어.

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

let p1: Point = { x: 10, y: 20};
p1.x = 20; // 여기서 에러가 나겠지?

TypeScript는 ReadonlyArray<T> 타입을 제공하고 있어. 그래서 생성 후에 배열을 변경하지 않게 할 수 있는거지.

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

여기 마지막 줄에서 일반 배열인 aReadonlyArray를 재할당 할 수 없는 걸 알 수 있겠지? 만약 오버라이드하고 싶으면 type assertion을 활용하면 돼. type assertion은 Basic Types 파트에서 배웠었어.

a = ro as number[];

**readonly vs const**

변수에는 const를 쓰고 프로퍼티에는 readonly를 사용하면 돼.

Excess Property Checks(초과 프로퍼티 검사)


optional properties 예제를 다시 한 번 보자.

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({colour: 'black', width: 100}); // 이 부분을 colour라고 바꿨어

JavaScript에서는 colour를 매개변수로 전달시킬 때 올바르게 썼다고 말할거야. 왜냐하면 width 프로퍼티는 정확하고 color는 선택적 프로퍼티이기 때문에 맞고, colour 프로퍼티는 중요하지 않기 때문이지.

하지만, TypeScript에서는 버그가 있다고 말해줄거야. 객체 리터럴을 다른 변수에 할당하거나 매개변수로 전달할 때, excess property checking을 받기 때문이지. 이게 무슨말이냐면, 객체 리터럴이 대상타입(여기서는 SquareConfig)이 갖고 잊지 않는 프로퍼티를 갖고 있다면 에러를 발생시킬거란 말이지.

만약 excess property checking을 피하고 싶으면 type assertion을 사용하면 돼.

(...)
let mySquare = createSquare({colour: 'black', width: 100} as SquareConfig);

만약 추가적인 프로퍼티가 있을거라고 확신한다면, string index signature(문자열 인덱스 서명)을 활용하는게 더 나은 방법이야.

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

검사를 피하는 또 다른 방법은 객체를 다른 변수에 할당할 수도 있어.

interface SquareConfig {
  color?: string;
  width?: number;
}
(...)
let squareOptions = { colour: 'red', width: 100};
let mySquare = createSquare(squareOptions);

let squareOptions = { colour: 'red'}; // 이렇게 공통 프로퍼티가 없으면 에러가 발생하겠지

이러면 squareOptions가 excess property checking을 받지 않기 때문에 에러가 발생되지 않아. 근데 유의할 점이 있는데, interface로 정의한 SquareConfig와 공통 프로퍼티가 존재해야 돼.

excess property check는 대부분 실제 버그이기 때문에 웬만하면, 검사를 피하지 마. 매우 복잡한 객체 리터럴에서는 그럴 수 있지만, 만약 초과 프로퍼티 검사 문제가 발생하면 타입 정의를 수정하는게 더 좋은 방법이야. 예를 들어, createSquarecolorcolour 모두 전달해도 괜찮으면 squareConfig를 수정해줘야겠지?

Function Types

함수의 매겨변수와 리턴 값의 타입을 정하는 방법이야. 마찬가지로 interface api를 사용해.

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

즉, SearchFunc을 타입으로 갖는 변수는 함수를 할당할 것이고, string 타입의 매개변수 두 개와 boolean 타입의 값을 리턴할거야. 다음 예제를 봐바.

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

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

mySearch = function(source: string, subString: string)
mySearch = function(src: string, sub: string) // 매개변수 이름이 같을 필요는 없어
mySearch = function(src, sub)

마지막 줄을 보면 타입이 정의되어있지 않지? 함수의 매개변수는 같은 위치에 대응되는 매개변수와 한 번에 하나씩 검사돼. 즉, SearchFunc 타입의 변수로 함수의 매개변수 값이 할당되기 때문에 TypeScript의 contextual typing이 자동으로 매개변수의 타입을 추론하는거지.

Indexable Types


interface로 인덱스의 타입을 기술할 수도 있어. 인덱싱 타입과 반환 값의 타입을 기술하는걸 index signature라고 해.

interface StringArray {
  [index: number]: string; // number로 인덱싱하면 string을 반환한다
}

let myArray: StringArray;
myArray = ['BOB', 'FRED'];

let myStr = myArray[0];
console.log(myStr);

인덱싱에 지원되는 타입은 두 가지야. numberstring

index signature는 프로퍼티의 반환 타입이 일치하도록 강제해.

interface NumberDitionary {
  [index: string]: number;
  length: number;
  name: string; // 에러, 반환값이 string으로 맞지 않기때문이지
}

위의 예제를 허용하고 싶으면 |를 사용하면 돼.

interface NumberDitionary {
  [index: string]: number | string;
  length: number;
  name: string; // 에러, 반환값이 string으로 맞지 않기때문이지
}

Class Types


인터페이스 구현하기

클래스에서 인터페이스를 적용할 때 implements를 사용하자.

interface ClockInterface {
  currentTime: Date;
}

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

클래스 안에 구현된 메서드도 인터페이스 안에서 정의할 수 있어.

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void; // 이렇게 메서드를 정의해
}

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

클래스 스태틱과 인스턴스의 차이점

생성 시그니처로 인터페이스를 생성하고, 이를 implements하여 클래스를 생성하려고 하면 에러가 발생해. 왜냐하면, 인터페이스를 implements할 때 클래스의 인스턴스만 검사하기 때문이야. 생성자는 스태틱이란 말이지.

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

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

// 에러

이게 에러가 나는 이유는 ClockConstructor는 인스턴스인지를 검사하는데 클래스 Clock은 인스턴스가 아닌 스태틱이기 때문이야.

따라서 이를 해결하기 위한 두 가지 방법을 소개할게.

<1>

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

interface ClockInterface {
  tick(): void;
}

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

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

class AnalogClock implements ClockInterface {
  constructor(h: number, m: minute) {}
  tick() {
    console.log('tick tok');
  }
}

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

ClockConstructor를 이해하기가 조금 까다로운데 생성자는 hourminute을 가지고 있으며 그렇게 생성된 인스턴스는 ClockInterface 타입인거지. 다시 말하자면, createClock 생성자 함수의 첫번째 매개변수 ctor(hour: number, minute: number)의 생성자로 만들어진 인스턴스이며 이는 tick() 메서드를 갖는 ClockInterface 타입이라는 말이야.

중요한 점은 ClockConstructor 인터페이스를 할당하는 건 인스턴스여야 한다는 점이야.

<2> 클래스 표현을 사용하면 간단하게 구성할 수 있어.

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

interface ClockInterface {
  tick(): void;
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log('beep beep');
  }
}

위의 두가지 방법 모두 인스턴스를 만들어서 ClockConstructor 인터페이스의 조건을 맞춰줬지?

인터페이스 확장하기

인터페이스도 클래스처럼 확장가능해.

interface Shape {
  color: string;
}

interface Square extends Shape {
  sidLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sidLength = 20;

여러 인터페이스를 확장할 수도 있어

interface Shape {
  color: string;
}

interface Value {
  price: number;
}

interface Square extends Shape, Value {
  sidLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sidLength = 20;
square.price = 30;

Hybrid Types (하이브리드 타입)

JavaScript의 유연하고 동적인 특성을 활용해 여러가지 타입의 조합을 가진 객체를 만들 수 있어. 예를 들면, 함수와 객체 역할을 동시에 수행하는 객체처럼 말이야.

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() {};
  return counter;
}

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

getCounter() 함수가 반환하는 값은 Counter 타입으로 매개변수는 number 타입이고 interval 프로퍼티와 reset() 메서드를 가지는 객체임을 말해주고 있지?

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

인터페이스가 클래스를 상속받았을 때 클래스의 멤버는 상속받지만 구현은 상속받지 않아. 또한 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() {}
}

// 에러!!
class Image implements SelectableControl {
  private state: any;
  select() {}
}

SelectableControlControl을 상속받고 있기 때문에 Control 혹은 Control의 하위 클래스에 의해서만 구현될 수 있어. Button, TextBox가 이에 해당되겠지?

Image 클래스가 왜 안되냐면 Control 클래스에 의해 만들어진 state여야만 하는데(private이기 때문에) Image 클래스에서 새롭게 정의한 state라서 호환이 안되는거야.

profile
세계 최고 수준을 향해 달려가는 개발자입니다.

0개의 댓글