Typescript Interface

TypeScript 핵심 원칙중 하나는 type를 검사할때 value가 가지고 있는 형태(shape)에 초점을 맞추는 것입니다. 이것을 “duck typing” 또는 “structural subtyping” 이라고 부릅니다. TypeScript에서 인터페이스는 이러한 type에 이름을 지어주는 역할을 하고 프로젝트 외부의 코드와의 계약 뿐만 아니라 코드 내부에서의 계약을 정의하는 강력한 방법이 됩니다.

첫번째 인터페이스

1
2
3
4
5
6
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

printLabel 호출에서 type을 검사합니다. printLabel 함수는 string type의 label property를 가지는 object를 한 개의 parameter로 받습니다. 실제로 object는 더 많은 property를 가지고 있지만 컴파일러는 필요한 최소한의 type만을 검사합니다.

이번에는 string type의 label property를 가지는 interface를 사용해서 예제를 다시 작성해 봅니다.

1
2
3
4
5
6
7
8
9
10
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue interface는 앞의 예제에서 요구 사항을 기술하는 데 사용합니다. 여전히 string type의 label이라는 단일 property을 갖는 것을 나타냅니다. printLabel에 전달한 object가 다른 언어에서와 같이 interface를 구현한다고 명시적으로 기술할 필요가 없었습니다. 여기서 중요한 것은 형태입니다. 함수에 전달 된 object가 나열된 요구 사항을 충족하면 허용됩니다.

Optional Property

interface의 모든 property가 필요할 수는 없습니다. 특정 조건 하에서 존재하거나 전혀 존재하지 않을 수도 있습니다. 이러한 선택적 property는 두 개의 property 만 채워지는 함수에 object를 전달하는 “option bags”과 같은 패턴을 만들 때 널리 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 property는 이름의 마지막에 ?를 붙여서 선언합니다.

Readonly property

property를 readonly로 지정하면 object를 생성하고 후에 수정할 수 없습니다.

1
2
3
4
5
6
7
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x:10, y:20 };
p1.x = 5; // 에러

TypeScript에서는 Array<T>에서 수정하는 method를 없앤 ReadonlyArray<T>를 제공합니다.

1
2
3
4
5
6
7
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[]; // type assertion 사용하면 가능

readonly vs const

변수는 const를 사용하고 property는 readonly를 사용합니다.

Excess Property Check

첫번째 예제에서 parameter로 { label: string; }를 받는 함수에 { size:number; label: string; }를 전달하는 것을 보았습니다. 또한 optional property과 이것이 “option bag”를 기술할때 유용하다는 것을 배웠습니다.

그러나 이 두가지를 단순하게 결합하면 어떻게 될까요? 마지막 예제를 다시 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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: "red", width:100});

여기서는 createSquare 함수에 color 대신 colour를 전달했습니다. 자바스크립트에서는 이런 것들은 조용히 버그를 유발합니다.

그러나 TypeScript에서는 코드에 버그가 있을 것이라는 입장을 보입니다. Object literal은 다른 변수에 할당되거나 인자로 전달될때 excess property checking를 거칩니다. object literal가 목표가 되는 type이 가지고 있지 않는 property를 가지고 있다면 에러가 발생합니다.

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

이 검사는 피하는 방법은 세가지가 있습니다.

  • type assertion
  • index signature를
  • 객체를 변수에 할당

가장 간단한 방법은 type assertion입니다.

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

그러나 객체가 특별한 방법으로 사용되는 몇 가지 추가 property을 가질 수 있다고 확신하는 경우 문자열 index signature를 추가하는 것이 더 나은 방법 일 수 있습니다. SquareConfigs가 위의 유형으로 색상 및 너비 property을 가질 수 있지만 다른 property도 여러 개 가질 수 있다면 다음과 같이 정의 할 수 있습니다.

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

이 검사를 피하는 마지막 방법은 객체를 다른 변수에 할당하는 것입니다. squareOptions가 excess property check를 거치지 않으므로 컴파일러에서 오류를 내지 않습니다.

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

위의 간단한 코드의 경우 이러한 검사를 피하려고 하면 안됩니다. 메소드와 상태를 유지하는 보다 복잡한 object literal의 경우 이러한 기법을 염두에 두어야 할 수도 있지만 대부분의 excess property error는 실제로 버그입니다. 즉 option bag과 같은 경우에 excess property checking 문제가 발생하는 경우 type 선언 중 일부를 수정해야 할 수도 있습니다. 이 경우 color 또는 colour 속성을 가진 객체를 createSquare에 전달하면 SquareConfig의 정의를 수정하여 이를 반영해야합니다.

Function Type

인터페이스는 JavaScript 객체가 취할 수있는 다양한 형태를 기술할 수 있습니다. 속성을 가진 객체를 기술하는 것 외에도 인터페이스는 함수 형태을 기술할 수 있습니다.

인터페이스가있는 함수 유형을 설명하기 위해 인터페이스에 call signature를 제공합니다. 이것은 주어진 매개 변수 목록과 반환 유형 만있는 함수 선언과 같습니다. 매개 변수 목록의 각 매개 변수에는 이름과 유형이 모두 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
if (result == -1) {
return false;
}
else {
return true;
}
}

파라미터의 이름이 일치할 필요하는 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string) {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}

기능 매개 변수는 한 번에 하나씩 검사되며 해당 매개 변수 위치의 유형이 서로 대조됩니다. type을 지정하지 않으면 function value가 SearchFunc type의 변수에 직접 할당되므로 Typescript는 문맥에서 argument type을 추론할 수 있습니다. 여기서도 함수 식의 반환 type은 반환하는 값 (여기에는 falsetrue)에 의해 암시됩니다. 함수식이 숫자 또는 문자열을 반환했다면 type checker는 반환 형식이 SearchFunc 인터페이스에 설명 된 반환 형식과 일치하지 않는다고 경고합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}

Indexable Type

함수 type을 설명하기 위해 인터페이스를 사용하는 방법과 마찬가지로 a[10] 또는 ageMap[ "daniel"]처럼 “색인을 생성”할 수있는 type을 설명 할 수 있습니다. 인덱싱 가능 type에는 indexing 할 때 대응하는 반환 유형과 함께 객체에 대해 indexing하는 데 사용할 수있는 유형을 설명하는 index signature가 있습니다. 예를 들어 보겠습니다.

1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];

위 예제에서 StringArraynumber로 indexing되며 string를 반환한다는 index signature를 기술하고 있습니다.

index signature에는 stringnumber의 두 종류가 있습니다. 두 종류를 같이 사용할 수 있지만 number index로 반환되는 type은 string index로 반환되는 type의 subtype이어야 합니다. 이는 자바스크립트에서는 number로 indexing를 하면 실제로 string로 바꿔서 indexing하기 때문입니다. number 100로 indexing하면 string "100"로 indexing 하는 것과 동일하므로 둘은 일관성이 있어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
interface NotOkay {
[x: number]: Animal; // 에러
[x: string]: Dog;
}

string index signature은 “dictionary”패턴을 기술하는 강력한 방법이지만 모든 속성이 return type과 일치해야 합니다. 이것은 string index가 obj.propertyobj[ "property"]로 사용할 수 있다고 선언하기 때문입니다. 다음 예제에서 name의 type은 string index의 type과 일치하지 않으며 type-checker는 에러를 발생시킵니다.

1
2
3
4
5
interface NumberDictionary {
[index: string]: number;
length: number;
name: string; // 에러
}

마지막으로 index signature를 readonly로 만들 수 있습니다.

1
2
3
4
5
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // 에러

Class Type

Implementing interface

C # 및 Java와 같은 언어로 인터페이스를 사용하는 가장 일반적인 방법 중 하나는 클래스가 특정 계약을 충족하도록 명시 적으로 적용하는 것이 TypeScript에서도 가능하다는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date): void {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}

Difference between the static and instance sides of classes

class에는 static side type과 instance side type이 있습니다.
construct signature가 있는 인터페이스를 implement하는 class를 정의하면 에러가 발생합니다.

1
2
3
4
5
6
7
8
interface ClockConstructor {
new (hour: number, minute: number): void;
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}

이것은 클래스가 인터페이스를 구현할 때 클래스의 instance side만 검사되기 때문입니다. 생성자는 static side에 있기 때문에이 체크에 포함되지 않습니다.

대신 클래스의 static side에서 직접 작업해야합니다. 이 예제에서는 constructor에 대한 ClockConstructor와 instance method에 대한 ClockInterface의 두 인터페이스를 정의합니다. 편의상 전달 type의 인스턴스를 생성하는 createClock constructor 함수를 정의합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);

createClock(AnalogClock, 7, 32)에서 createClock의 첫 번째 매개 변수는 ClockConstructor type이므로 AnalogClock이 올바른 constructor signature을 갖고 있는지 확인합니다.

Extending Interface

class와 같이 interface는 extend할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

여러개의 interface를 확장할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 Type

이전에 언급했듯이 인터페이스는 실제 JavaScript에서 제공되는 풍부한 type을 기술 할 수 있습니다. JavaScript의 역동적이고 유연한 특성으로 인해 위에 설명 된 일부 type의 조합으로 작동하는 객체가 종종 발생할 수 있습니다.

이러한 예는 추가 속성을 사용하여 함수와 object의 역할을 모두 수행하는 object입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

3rd-party JavaScript와 상호 작용할 때 type 형태를 완전히 기술하려면 위와 같은 패턴을 사용해야 할 수 있습니다.

Share