Interfaces
TypeScript Interface 설명
목차
-
Introduction
-
Optional properties
-
Readonly properties
-
Excess preperty checks
-
Function types
-
Indexable types
-
Class Types
-
Extending interfaces
-
Hybrid Types
-
Interfaces extending Classes
Introduction
타입스크립트의 핵심 원칙은 타입체킹이 value가 가지는 모양에 초점을 맞추고 있다는 점이다. 이것은 duct typeing 혹은 structural subtyping 이라고도 불린다. 타입스크립트에서는 interface들은이런 타입들에 이름을 붙이는 역할을 하고, 당신의 혹은 다른사람과의 프로젝트에서 계약을 결정짓는 강력한 기능을 한다.
우리의 첫번째 interface
인터페이스가 어떻게 작동하는지 이해하는 가장 쉬운 방법은 아래의 예시를 시작하는 것이다.
function printLabel(labelledObj: { label : string})
{
console.log(labelledObj.label);
}
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj);
타입체커는 printLabel 함수를 불러온다. printLabel 함수는 하나의 인자값을 가지고 있는데, 이 인자값은 label이라는 내부 prop을 가지고 string타입을 가지길 요구한다. 주목하라. MyObj가 label이외의 값을 가지고 있다는 점에. 컴파일러는 적어도 하나 이상의 요구된 값의 타입이 맞는지 확인한다. 몇가지 타입스크립트가 허용하지 않는 가지수가 있는데 그것들은 나중에 다룰 것이다.
주석 : 타입스크립트 컴파일러는 요구된 값의 이름과 타입이 일치하기만 한다면 다른 값이 추가되어도 상관하지 않는다고 본다. 단, 인자값으로 넘어가서 함수 내에서 사용하려한다면 error를 발생시킬 것이다.
우리는 같은 예시를 다시 쓸수 있는데, 이번에는 우리는 interface를사용하여 내부에 있는 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의 인자값으로 동일하게 요소값이 lable인 string 타입의 값을 요구한다. 하지만 구현시에 처음 예제와 같이 직접 인자값에 요구값과 타입을 명시하지 않고, LabelledValue라는 interface 이름으로 넘겼다는 점이 차이다. 타입스크립트의 interface에서 중요한 점은 myObj가 interface를 implements 하지 않았음에도 printLabel에서 사용할 수 있다는 점이다. 다른 언어의 경우에는 myObj가 반드시 LabelledValue를 implements 해야만 printLabel에 인자값으로 넣을 수 있다는점과 차이가 있으며, 타입스크립트가 interface를 보는 것이 아니라 interface가 구현하는 값이 무엇인지 확인한다는 점을 알 수있다.
한가지 지적해야할 점은 interface가 요구하는 값의 순서가 상관이 없고, 오로지 객체가 그 값들의 타입을 만족 시키는지만 확인한다.
Optional Properties
모든 prop이 필수 요소가 아닐 수도 있다. 어떤 경우에는 있을수도, 그리고 없을 수도 있는 prop이 있을 수도 있다. 함수의 인자값으로 넘기는 object에서 오직 몇개의 요소만이 interface의 요소를 만족시킬 경우에 optional bags와 같은 패턴을 만들때 이런 optional prop는 유용하다.
여기 이 패턴의 예시를 적어놨다.
interface SqureConfig {
color?: string;
width?: number;
}
function createSquare(config: SqureConfig) : {color: string; area:number} {
let newSqure = {color: 'white', area: 100};
if(config.color) {
newSquare.color = config.color;
}
if(config.width) {
newSquare.area = config.width ** 2;
}
return newSquare;
}
let mySquare = createSquare({color:'black'});
Interfaces 의 표기방식은 다른 interface들의 구현방식과 비슷하지만, :
왼쪽에 property 이름 바로 오른쪽 끝부분에 ?
를 표기함으로써 구현할 수있다.
optional interface의 장점은 optional인 인자값이 있던 없던, interface이외의 값이 인자값으로 넘어 갔을 경우에 error를 발생시킬 수 있다는 점이다. 예를 들자면 아래와 같은 코드에서는 에러가 발생한다.
interface SqureConfig {
color?: string;
width?: number;
}
function createSquare(config: SqureConfig) : {color: string; area:number} {
let newSqure = {color: 'white', area: 100};
if(config.colr) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.colr;
}
if(config.width) {
newSquare.area = config.width ** 2;
}
return newSquare;
}
let mySquare = createSquare({color:'black'});
주석 : 만약
{color:'black', something:20}
으로 인자 obj를 직접 적어줬다면 something이 interface에 없기 때문에 에러가 터진다. 하지만 대신let myObj = {color:'black', something:20}
을 선언하고 인자값으로createSquare(myObj)
를 할경우 error가 나지 않는다. 동일한 obj이지만 literal일경우 함수가 호출될때 걸러내지만, 그렇지 않을 경우 걸러내지 않는다. 단, function내부에서는 오로지 interface의 prop만 사용 가능하기때문에 다른 값이 들어간다고 하더라도 문제가 없다.
Readonly properties
몇몇 prop들은 오직 처음에 만들어졌을 상황으로 유지되어야 할 때가 있다. 당신은 이런 환경을 readonly
를 prop 이름 앞에 붙임으로서 만들어 낼 수 있다.
interface Point {
readonly x: number;
readonly y: number;
}
당신은 Point를 object literal에 할당할수가 있다. 할당 이후에는 x와 y값의 변경은 불가능해진다.
let p1: Point = { x: 10, y: 20};
p1.x = 5; //error
타입스크립트는 배열역시 readonly로 만들 수 있는 interface를 구비하고 있다. ReadyonlyArray를 선언함으로써 Array내부에 선언된 값들을 변경시킬 수 없게 만든다.
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // -> error
ro.push(5) // -> error
ro.length = 100 // -> error
a = ro; // error
마지막 라인에서 ReadonlyArray의 reference 할당조차 컴파일러가 error로 걸러낸다는 것을 볼 수 있다. 그렇지만 referece를 할당하는 방법이 있는데 타입 캐스팅을 통해 할당이 가능하다.
a = ro as number[];
readonly vs const
readonly또는 const를 써야할지 말지 기억하는 가장 쉬운 방법은 당신이 변수를 쓰고 있는건지 아니면 prop를 쓰고 있는지다. const는 변수고 readonly는 prop에 사용한다.
Excess Property Checks
우리의 첫번째 interface 예시에서 타입스크립트는, 인터페이스에서는 { label: string}
만 요구하는데 함수 인자값으로 { size: number, label: string}
인 객체를 넘겼다. 우리는 또한 optional property에 대해 배웠고 이것이 얼마나 option bags라는 것을 구현하기에 유용한지 배웠다.
그러나 순진하게 두가지를 모두 섞는다면 JS에서 했던 실수를 똑같이 할 것이다. 위와 같은 예제의 createSquare로 다시 설명한다면:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config : SquareConfig) : { color: string; area: number} {
//....
}
let mySquare = creatSquare({ colour: 'red', wdith:100});
인자값으로 오타가 들어갔다는 것을 주의깊게 보라. (color 대신에 colour가 들어갔다.) 그냥 javascript에서는 조용히 실패를 하게 될 것이다.
당신은 이 프로그램이 제대로 타입되었다고 예기할 수도 있다. 왜냐하면 width prop이 맞는 방식으로 들어갔기 때문이다. 그리고 추가적으로 color는 존재하지 않고 colour prop은 중요하지 않기 때문이다.
그러나 타입스크립트는 이코드에서 에러를 발생시킨다. Object literals은 특별한 취급을 갖고 excess property checking을 실행한다. 이것은 변수에 할당하거나 인자값으로 넘길때 발생하는데, 만약 interface에 없는 값을 직접 Object literals로 추가하려고 할경우 타입스크립트는 허용하지 않을 것이다.
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({
colour: 'red',
width: 100
});
이런 류의 에러는 인자값을 넣을 때 원하는 인터페이스로 타입캐스팅을 하기만 하면 빠져나갈 수있다.
let mySquare = createSquare({
width: 100,
opacity:0.5
} as SqaureConfig);
더 나은 방법은 애초에 inteface를 만들때 더 propName을 추가 할 수 있도록 구현하는 것이다. 이것을 index signature를 추가한다고 하며 아래와 같이 구현한다.
interfacet SquareConfig {
color?: string;
width?: number;
[propName: string]: any; //이제 어떤 prop이든 추가할 수 있다.
}
위와 같이 구현하면 몇개의 prop이든 어떤 값이든 추가가 가능하며 color와 width만 타입의 조건을 충족만 시킨다면 통과시킨다.
마지막으로 excess property check를 벗어나는 방법은 인자로 넘길 객체를 다른 객체에 할당한 후에 인자값으로 넘기는 방법이다. 이렇게 할경우 에러를 발생시키지 않는다.
let squareOptions = { colour: 'red', width: 100};
let mySquare = createSquare(squareOptions); //no error
위와같은 코드는 주로 의도치 않은상황에서 발생한다. 더 복잡한 메서드나 상태를 지니고 있는 object literals의 경우에는 당신은 이런 태크닉에 대해 알고 있어야 한다. 왜냐하면 대부분의 object literal 에러는 버그이기 때문이다. 웬만하면 정의되지 않은 값을 넘기는 것보다는 interface의 재정의를 할 것을 추천한다.
Function Types
Interface는 javascript의 다양한 형태를 구현할 수 있을 뿐 아니라 함수타입 역시 구현이 가능하다. 함수타입을 interface에 구현하기 위해, 우리는 call signature라는 것을 주었다. 이것은 함수 선언과 같은데 함수의 인자값으로 어떤 값이 들어값이 어떤 타입을 가지고 있을 것인가와 반환값의 타입이 무엇일 것인지를 명시해 줌으로써 함수 타입이라는 것을 선언한다. 아래 코드와 같이 함수를 선언해준다.
interface SearchFunc {
(source: string, subString: string): boolean;
}
한번 선언되면, 다른 인터페이스처럼 사용할 수가 있다. 여기서 우리는 어떻게 변수에 function 타입을 만들고, 같은 타입의 function을 할당하는지 보여줄 것이다.
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
반드시 처음에 interface에 선언한대로 인자값이 들어갈 필요는 없다. 우리는 아래와 같이 함수를 할당 할 수 있다.
let mySearch: SearchFunc;
mySearch = function(src: string, sub:string): boolean
{
let result = src.search(sub);
return result > -1;
}
만약 인자값으로 src나 sub에 다른 타입을 넘기려고 하거나 리턴값으로 boolean아 아닌 다른 값을 반환할려고 한다면 에러를 발생시킬 것이다. 왜냐하면 인자값에 type어 어떤 것이 사용되었고 interface에 선언된 함수타입 선언과 일치하는지 하나하나 컴파일러가 확인할 것이기 때문이다.
Indexable Types
함수를 인터페이스에 어떻게 사용할지와 비슷하게 우리는 또한 a[10] 또는 ageMap[‘daniel’]과 같이 인덱싱할 수있는 타입을 구현할 수있다. Indexable Type들은 인덱싱할 때 사응하는 리턴타입에 따라 _index signature_를 가진다. 어려운 설명인 것 같다. 아래의 예시를 보며 설명하겠다.
interface StringArray {
[index: number]: string; //반환하는 타입이 무엇인가.
}
let myArray: StringArray;
myArray = ['bob', 'Fred']; //배열일지 모르지만 사실 기본적으로 객체이다.
let myStr: string = myArray[0];
위의 예제에서 StringArray interface는 index signature 를 가지고 있다. StringArray가 number로 index 될 때 string을 반환할 것이다.
주석 : 저게 어떻게 되나 싶지만 사실 알고보면 배열은 이렇게 생긴 객체다
{
'1' : 'bob,
'2' : 'fred'
}
저렇게 인덱싱 가능하게 interface를 만들어 놓으면 몇개의 값이든 더 들어갈 수 있되, 숫자로 인덱싱이 가능하다.
string과 number 오직 2가지타입만 인덱시에서 지원한다. 하지만 결국 숫자로 index 되어도 string과 같다는 것을 기억해야한다. javascript는 사실 오브젝트로 인덱싱하기 전에 숫자를 문자열로 변환된다. 아래의 예제를 보자.
class Animal {
name: string;
}
class Dog extends Animal{
breed: string;
}
//Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
let animal: Animal = new Animal();
let dog: Dog = new Dog();
let somethings: NotOkay = {
[0]: animal // -> error
//[0]: dog -> ok
}
위의 코드가 에러인 이유는 숫자로 인덱싱해도 사실상 문자열로 indexing하는 것과 동일한데, 0으로 인덱싱 하자니 숫자 인덱싱이여서 Animal을 반환해야 한다고 생각되지만, 사실은 문자열이니 반환값은 Dog class여야 하기 때문이다. 그래서 Dog가 반환되어야 하기때문에 number로 인덱싱할 경우 문제가 생긴다. 하지만 Dog를 숫자로 인덱싱 할 경우 문제가 없는데, 0이 문자열로 인덱싱 되어 Dog class가 반환되는 것이 맞기때문이다.
마지막으로 index signature를 readonly를 입힐 수 입혀 변경을 방지할 수 있다.
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ['Alice', 'Bob'];
myArray[2] = 'Mallory'; // -> error!
위와 같이 myArray에 다른 값을 할당하는것은 error를 발생 시킨다.
Class Types
Implementing an interface
가장 평범한 형태의 iface(interface) 사용은 class가 어떤 특정한 형태를 유지하게 만드는 것이며 이것은 ts에서 가능하다.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number);
}
또한 메서드를 interface에 선언하고 implements 된 class가 구현하게 강제할 수 있다. 아래의 예제를 보자
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
iface는 public과 private사이드 모두를 구현하기보다는 public사이드를 구현한다. 덕분에 클래스에서 private side가 있는지 아닌지 확인하는 것을 막는다.
Difference between the static and instance sides of classes
클래스와 iface를 다룰 때, class가 두가지 성질을 가지고 있다는 점을 마음에 두고 있다면 도움이 될 것이다. 첫번째는 static side이고 하나는 instance side이다. 당신은 아마 이미 눈치 채고 있을 것이다. constructor의 형태를 정의한 iface를 implements한 클래스는 error를 발생시킨다는 것을. 왜냐하면 implements 했을 때 interface가 체크하는 것은 오로지 instance사이드의 요소들을 체크할 뿐 static 사이드는 검사하지 않기 때문이다.(constructor는 static 사이드에 있다.)
interface ClockConstructor {
new (hour: number, minute: number): Object;
}
class Clock implements ClockConstructor { // error
currentTime: Date;
constructor(h: number, m: number) {}
}
대신에, class의 static 사이드를 직접 건드려야 할것이다. 아래의 example에서는 우리는 두개의 iface를 정의했는데, 하나는 ClockConstructor이고(이름과 동일하게 constructor를 정의하였다.) 나머지 하나는 ClockInterface이며 instance 사이드를 규정할 것이다. 다음 편의를 위해 createClock이라는 함수를 정의하였으며, 이 함수는 인자로 넣은 클래스의 instance를 반환한다.
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
function createClock(ctor: ClockConstrucgtor, 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);
createClock의 첫번째 인자값으로 생성자의 형태를 체크한다. 위의 예제에서는 createClock(AnalogClock, 7, 32)
에서 AnalogClock
의 성성자가 ClockConstructor
iface의 생성자 형태와 맞는지 체크한다.
Extending Interfaces
클래스처럼 iface는 서로 상속할 수가 있다. 이것은 다른 iface의 멤버들을 또다른 iface에 이식시키는 것이 가능하게 만든다. 이 기능은 유용성을 제공해 주고 재활용 가능한 컴포넌트를 만드는데 일조할 것이다.
interface Shape {
color: string;
}
interface Square extends Shaep {
sideLength: number;
}
let square = <Square>{};
square.color = 'blue';
square.sideLength: 10;
하나의 인터페이스는 여러개의 인터페이스들을 한꺼번에 상속받을 수 있다.
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
위에어 언급했듯이 iface는 자바스크립트의 풍부한 타입들을 표현해 낼 수 있다. 자바스크립트의 유연하고 역동적인 특성때문에, 여러타입을 동시에 가지고 있는 객체들을 다룰일이 생길 것이다. 대표적인 예로 함수객체로, 함수는 객체도 될 수 있고 함수도 될 수 있다.
아래의 예시와 같은 방법으로 함수객체의 여러타입을 다룰 수 있다.
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;
}
3rd-party js를 사용해야 할 때, 당신은 타입의 형태를 최대한 구현하기위해서 위와 같은 패턴을 사용해야 할 것이다.
Interfaces Extending Classes
iface가 class를 상속받을 때, class의 멤버는 상속받지만 구현된 내용은 상속받지 않는다. 이것은 마치 구현을 제공하지 않고 클래스의 맴버들을 iface에 선언한 것과 같다.iface는 public이나 private 멤버들 전부 상속받는 것이 가능하다.이건 당신이 private나 protected 멤버를 클래스로부터 iface에 상속할때 그 인터페이스를 implements하는 것은 상속받은 class이거나 그 하위 클래스일때만 가능하다는 것을 의미한다.
주석: implements멤버는 모두 구현해야만 하는데 class를 상속받은 iface는 class의 멤버도 구현하기를 요구한다. 그렇기 때문에 당연하게도 iface를 implements한 class는 implements가 상속받은 class의 멤버까지 구현하길 요구하는 것이다.
당신이 대규모의 상속구조를 가지고 있고, 특정한 class의 멤버들만 구현되기를 바랄때 유용할 것이다. 게다가 subclass들은 굳이 base 클래스일 필요는 없다. 예를 들자면
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 {
select() {}
}
class Location{
}
위의 예졔에서 SelectableContorl
은 state
프라이빗 멤버까지, Control
의 모든 멤버를 iface가 포함하고 있다.
state
는 Control
의 프라이빗 멤버이기 때문에 오직 Control
을 상속받은 class
만이 SelectableContorl
을 implements
할 수 있다. Control
을 상속받은 클래스 내에서는 SelectableControl
을 implements
하여 state
프라이빗 멤버에 접근하는 것이 가능하다. 효과적으로 SelectableControl
은 Control
처럼 행동하며 select
메서드를 가져야 한다.
Button
과 TextBox
는 Control
을 상속했기 때문에 SelectableControl
을 implements
할 수 있지만 Image
와 Location
클래스는 그렇지 않기 때문에 state 프라이빗이 맴버가 없기 때문에 에러를 발생시키고 SelectableControl
을 implements 할 수 없다.