5. typescript - interfaces

2020. 3. 3. 23:58typescript/typescript-grammar

타입 스크립트 중 가장 중요한 원리 중 하나는 값의 모형에 맞추어 집중하여 타입을 체크하는 것입니다. 이것은 duck typing(덕 타이핑) 또는 구조적 서브타이핑(structural subtyping)입니다. 

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

let myObj = { size: 10, label: "size 10 Obj" };

printLabel(myObj);

위 코드를 보면 labelObj를 받아서 비구조화 할당으로 label의 타입을 체크하고 있습니다. label이 string이라면 출력해주고 있습니다. 그러나 우리가 사용한 객체는 size라는 property를 갖고있는데도 타입 체크를 통과하고 넘어 갔습니다. 

 

이번에는 직접적으로 interface를 사용하여 동일한 예제를 만들어 보겠습니다. 

interface LabeledValue {
  label: string;
}

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

let myObj2 = { size: 10, label: "Size 10 Object" };
printLabel2(myObj2);

 

위의 경우 LabeledValue에는 하나의 프로퍼니 string의 타입의 label을 갖고 있습니다. printLabel2는 LabeledValue라는 타입의 파라미터를 받기때문에 다른 언어의 경우에는 불가능하지만 덕 타이핑 구조적 서브 타이핑이 가능한 타입스크립트에서는 위의 기능을 제공합니다. 

 

위의 기능이 가능한 조건은 사용하려는 객체가 LabeledValue의 property를 모두 갖고 있으면 됩니다. 

 

optional Properties

 

optional Properties는 인터페이스에서 사용해도 되고 안해도 상관없는 property이다. 

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" });

사용법은 다른 인터페이스와 비슷한데 ?를 추가로 표기해준다. 

 

optional properties의 이점은 interface의 이용할 수 있는 properties를 이용하면서 인터페이스에 속하지 않은 properties로 부터 보호 받을 수 있습니다.

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.colr) newSquare.colr = config.color; // Error: Property 'clor' does not exist on type 'SquareConfig'
  if (config.width) newSquare.area = config.width * config.width;
  return newSquare;
}
let mySquare = createSquare({ color: "black" });

 

Readonly properties

readonly properties는 처음 객체에 값을 할당해줄 때를 제외하고 값을 바꿀 수 없습니다. 

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

let point: Point = { x: 12, y: 14 };
point.x = 14; // error

 

또한 배열 타입 중에 ReadonlyArray<T> type이 있는데 Array<T>랑 변경할 수 있는 함수가 모두 지워진 것을 제외하고 모두 같습니다. 

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

위와 같이 변경하는 코드는 모두 에러가 납니다. 마지막 줄에는 처음에 의아할 수 있지만 ReadonlyArray를 일반 Array에 삽입하는 것은 안됩니다. 그래서 변경하려는 배열 타입으로 변경하면서 대입해주어야 합니다. 

a = ro as number[];

 

readonly vs const 

const는 일반적인 변수에 사용되고 const는 property에 흔히 사용합니다. 

 

Excess Property Checks

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

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

  return { color: "test", area: 12 };
}

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

위의 코드를 보면 함수가 받는 값에 colour로 잘못 입력된 값이 있습니다. js의 경우 조용하게 에러가 납니다. 

타입 스크립트의 경우 이 코드의 경우 버그가 발생합니다. 객체 리터럴은 특별히 처리해주고 다른 변수에 할당해줄 때나 파라미터에 넘겨 줄 때 엄격한 property check을 겪습니다. 대상 property가 없으면 에러가 발생합니다.

// error: Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
let mySquare = createSquare({ colour: "red", width: 100 });

또한 쉬운 방법으로는 type을 변경해주면서 입력해주면 됩니다. 

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

하지만 더 좋은 방법은 추가적인 property를 받는 다면 사용하는 특별한 방법이 있습니다. 이전 SquareConfig에서 color, width말고 property가 있을 수 있다면 우리는 아래와 같이 정의할 수 있습니다. 

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

위와같이 interface를 작성하면 color, width를 제외한 다른 property가 와도 에러가 발생하지 않습니다. 이것을 확인해보는 가장 좋은 방법은 실제로 대입해보는 것입니다. 실제로 에러가 발생하지 않습니다. 

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

하지만 일러한 체크를 피할려고 하면 안됩니다. 메서드와 상태를 유지하는 더 복잡한 객체의 경우 이와 같은 방법을 유지하고 싶겠지만 초과 프로퍼티 체크의 경우 실제로 에러입니다. 

 

Function 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(source: string, sub: string) {
  let result = source.search(subString);
  return result > -1;
};

 

함수 파라미터는 한번 검사하는데 각각의 파라미터들을 타입과 위치가 일치하는지 검사합니다. 모든 것에 정확히 타입을 원하지 않으면 타입스크립트는 문맥적 타이핑을 할때 SearchFunc타입의 변수에 할당할 때 추론할 수 있습니다. 또한 우리 함수의 리턴 타입 표현은 값들로 함축되어 있습니다. 

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

 

타입스크립트가 자동으로 타입을 추론하기 때문에 위와같은 코드는 문제가 없습니다. 그러나 만약 반환 값이 string 또는 number라면 에러를 지적합니다. SearchFunc의 반환 타입과 일치하지 않기 때문입니다. 

mySearch = function(src, sub) {
  let result = src.search(sub);
  return "";
};

 

Indexible Types

인터페이스에서는 인덱싱을 할 수 있는 타입을 만들 수 있습니다. ex) a[0], a["name"] 사용법은 []안에 인덱싱할 타입을 작성하고 또한 그 배열안에 타입과 일치하는 반환 타입을 갖습니다. 

interface stringArray {
  [index: number]: string;
}

let myArray: stringArray;
myArray = ["bob", "Fred"];

let myStr: string = myArray[0];

 

위 코드의 인터페이스를 보면 index의 값이 number로 결정되어 있습니다. 이는 인덱싱을 숫자로 한다는 의미입니다. 또한 반환 타입으로 string으로 되어 있는데 이는 index의 안의 value입니다. 

 

지원되는 인덱서의 타입은 string과 number두가지 입니다. 인덱서의 두가지 타입을 사용할 수 있지만 반환하는 numeric indexer의 string indexer의 서브타입이여야 합니다. 왜냐하면 number 인덱싱은 자바스크립트에서 객체를 인덱싱 하기전에 string을로 바꾸기 때문입니다. 그 의미는 100을 "100"으로 변환한다는 이야기 입니다. 

 

class Animal {
  name: string;
}

class Dog extends Animal {
  breed: string;
}

interface NotOkay {
  [x: number]: Animal;
  [y: string]: Dog;
}

문자열 인덱싱은 딕셔너리 패턴을 만드는 강력한 방법입니다. 그러나 return type은 정확히 맞춰주어야 합니다. string index는 obj.property 또는 obj["property"]로 작성할 수 있습니다. 

interface NumberDictionary {
  [index: string]: number;
  length: number; // ok, length is a number
  name: string; // error, the type of 'name' is not a subtype of the indexer
}

그러나 | 연산자를 추가하여 사용할 수 있습니다. 

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number;
  name: string;
}

 

또한 readonly를 사용하여 property를 구성할 수 있습니다. 그래서 재할당이나 데이터를 추가하는데 에러가 발생합니다. 

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

let myArray2: ReadonlyStringInterface = ["test", "test2"];
myArray2[0] = "123"; //

 

classType

c# 또는 java처럼 typescript도 클래스에 직접 구현하여 사용할 수 있습니다. 

interface ClockInterface {
  currentTime: Date;
}

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

 

또한 method도 class에서 직접적으로 구현할 수 있습니다. 

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

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

 

인터페이스는 public 측면과 private 측면이 아닌 public 측면의 class를 만듭니다. class를 사용하여 클래스 인스턴스의 private 측의 특정 타입을 검사하는 것은 금지됩니다. 

 

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

클래스와 인스턴스를 사용할 때 클래스에는 두가지 타입이 있다고 생각해야합니다. 스태틱과 인스턴스 타입 두가지가 있습니다. 만약 우리가 constructor signature와 함께 인터페이스를 사용한다면 이 인터페이스를 구현한 클래스 또한 같이 사용할 것입니다. 

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

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

 

위 코드는 에러가 발생합니다. 왜냐하면 인터페이스를 클래스에 구현해줄 때 인스턴스 사이드에서는 타입을 체크합니다. 스태틱 사이드에서는 이러한 체크가 포함되지 않습니다. 

 

그래서 위 코드의 클래스를 static화하게 만드는 것이 중요합니다. 우리는 constructor을 위한 인터페이스 하나 인스턴스를 위한 인터페이스를 하나 만듭니다. 그러고 나서 편의를 위해 createClock을 만듭니다

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: 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);

 

Extending Interfaces

클래스 같이 인터페이스도 확장이 가능하다. 하나의 인터페이스의 멤버들을 다른 것에 복사하는 것이 가능합니다. 이는 융통성 있게 인터페이스를 분리하여 재사용성을 높일 수 있습니다. 

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = {} as Square;
square.color = "yellow";
square.sideLength = 100;

extends는 여러개의 인터페이스를 적용할 수 있다.

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;

 

Hybrid Types

js에서 다양한 타입들을 타입스크립트에서도 당연히 만들 수 있습니다. 이는 자바스크립트의 다이나믹하고 융통성 있는 환경 때문입니다. 그렇기 때문에 때론 다양한 타입이 정리되어 있는 객체를 만나기도 합니다.

 

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

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

 

Interfaces Extending Classes

인터페이스가 클래스 타입을 확장할 때 클래스의 멤버들은 상속하지만 그들을 구현하지 않습니다. 

이는 인터페이스가 구현하지 않고 클래스의 모든 멤버들을 선언하는 것과 같습니다. 인터페이스 상속은 클래스의 private와 protected 멤버를 갖습니다. 너가 인터페이스를 만들 때 private or protected members와 함께 클래스를 확장하는 인터페이스는 오로지 클래스 및 서브 클래스를 구현할 수 있다. 

 

class Control {
    private state: any;
}

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

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

class TextBox extends Control {
    select() { }
}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
    private state: any;
    select() { }
}

class Location {

}

 

위 예제에서 SelectableControl은 Control의 member를 갖고 있습니다. state는 private member이기때문에 Control의 자식들 만이 SelectableControl 구현하기 위한 Control의 자손입니다. Control의 자식들만이 같은 선언에서 시작된 private state 변수를 가지기 때문입니다. 이는 private member들이 호환가능해야 합니다. 

 

control 클래스 내에서 SelectableControl을 통해 state 변수에 접근할 수 있습니다. SelectableControl은 알려진대로 Select method를 가진 Control과 같이 행동합니다. 

 

'typescript > typescript-grammar' 카테고리의 다른 글

typescript - this and arrow functions  (0) 2020.03.06
6. typescript - class  (0) 2020.03.06
4. typescript - 변수 선언  (0) 2020.02.29
3. typescript - 타입  (0) 2020.02.27
2. typescript - 개발환경 설정  (0) 2020.02.27