Interface
- TypeScript의 핵심 원리 중 하나
- 타입 검사 시 값이 가지는 형태에 초점을 맞춘다 (structural subtyping)
- 컴파일 후 사라짐
- 선언과 구현 중, 선언만 존재
- 객체의 변수 및 메소드 집합이 객체의 타입을 결정하는 것
- 클래스에서 구현부가 빠졌다고 이해하면 편함
- 실질적인 구현은 구현한다고 선언한 클래스에 맡김
1) 첫번째 인터페이스 (Our First Interface)
/* 1. 인터페이스 사용 X */
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
// OK. 필요한 속성이 '최소한' 있고 타입이 맞는지 체크.
- 실제로 더 많은 프로퍼티를 갖고 있어도 컴파일러는 최소한 필요한 프로퍼티가 있는지와 타입이 잘 맞는지만 검사
/* 2. 인터페이스 사용 O */
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label); // Error!
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
- 함수에 전달된 객체가 나열된 요구 조건을 충족하면 허용된다
- 인터페이스를 여러 곳에서 재사용 가능
2) 선택적 프로퍼티 (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;
// newSquare.color = config.clor; // 오류!
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({ color: "black" });
3) 읽기 전용 프로퍼티 (Readonly properties)
- 객체 생성 시 값을 할당한 후, 읽기만 가능
interface Point {
readonly x: number; // 읽기 전용 프로퍼티
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // 오류!
ReadonlyArray
타입
- 생성 후 변경할 수 없는 배열!
let a: number[] = [1, 2, 3, 4];
let ra: ReadonlyArray<number> = a;
ra[0] = 12; // 오류!
ra.push(5); // 오류!
ra.length = 100; // 오류!
a = ra; // 오류! => 일반 배열에 다시 할당하는 것도 불가능
a = ra as number[]; // 가능! => 타입 단언(assertion)을 통해 오버라이드
readonly
vs const
- 변수에서 사용할 때 ->
const
- 프로퍼티에서 사용할 때 ->
readonly
4) 프로퍼티 초과 체크 (Excess Property Checks)
- 프로퍼티 초과 체크란?
- "대상 타입"에 없는 프로퍼티 => 오류
- 체크하는 케이스?
- 다른 변수에 할당하거나 매개변수로 전달할 때
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
// 오류 : 'colour'는 'SquareConfig' 타입에서 필요하지 않습니다.
프로퍼티 초과 체크를 피하는 3가지 방법
- 1번째 : 타입 단언(type assertion)을 사용
let mySquare2 = createSquare({ borderColor: "red", width: 100 } as SquareConfig);
- 2번째 (더 나은 방법) : 추가 프로퍼티가 있는 것이 확실한 경우, 문자열 인덱스 시그니처(string index signature)을 추가
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
- 3번째 : 객체를 다른 변수에 할당
interface SquareConfig {
color?: string;
width?: number;
}
let squareOptions = { colour: "red", width: 100 };
// quareOptions가 추가 프로퍼티 검사를 받지 않기 때문에, 에러X
let mySquare = createSquare(squareOptions);
위의 예제는 squareOptions
와 SquareConfig
사이에 공통 프로퍼티(width)가 있기 때문에 에러 발생하지 않은 것.
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions); // Error!
위처럼 간단한 코드의 경우, 이 검사를 "피하는" 방법을 시도하지 않는 것이 좋습니다. 메서드가 있고 상태를 가지는 등 더 복잡한 객체 리터럴에서 이 방법을 생각해볼 수 있습니다. 하지만 초과 프로퍼티 에러의 대부분은 실제 버그입니다. 그 말은, 만약 옵션 백 같은 곳에서 초과 프로퍼티 검사 문제가 발생하면, 타입 정의를 수정해야 할 필요가 있습니다. 예를 들어, 만약 createSquare에 color나 colour 모두 전달해도 괜찮다면, squareConfig가 이를 반영하도록 정의를 수정해야 합니다.
5) 함수 타입 (Function Types)
- 함수 타입의 표현을 위해 호출 시그니쳐(call signature)를 제공
interface SearchFunc {
(source: string, subString: string): boolean; // 호출 시그니쳐(call signature)
}
- 매개변수 이름은 같을 필요 없음
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean { // 매개변수 이름은 같을 필요 없음
let result = src.search(sub);
return result > -1;
}
SearchFunc
타입의 변수로 직접 함수 값이 할당되었기 때문에 TypeScript의 문맥상 타이핑 (contextual typing)이 인수 타입을 추론할 수 있다
let mySearch: SearchFunc;
mySearch = function(src, sub) { // 매개변수 타입 지정 안해줘도 contextual typing으로 추론
let result = src.search(sub);
return result > -1;
}
6) 인덱싱 가능 타입(Indexable Types)
a[10]
또는ageMap["daniel"]
처럼 타입을 인덱스로 기술
- 인덱서블 타입은 타입을 기술하는 인덱스 시그니처 (index signature)를 가지고 있다
- Index signature는 2가지 타입만 가능: 문자열, 숫자
interface StringArray {
[index: number]: string; // 인덱스 시그니처(index signature)
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0]; // Bob
인덱싱 타입 혼합 (문자열 + 숫자)
- 숫자 인덱서에서 반환된 형태는 문자열 인덱서에서 반환된 형태의 하위 형태여야 함
- number를 사용하여 색인을 생성할 때 JavaScript가 객체로 색인하기 전에 문자열로 변환하기 때문
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
interface NotOkay {
[stringIndex: string]: Dog;
[numberIndex: number]: Animal;
}
읽기전용 Index signature
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // 오류!
7) 클래스 타입 (Class Types)
implements
로 Class에 적용
- public만 기술 가능
- 클래스 인스턴스의 private에 특정 타입이 있는지 확인X
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
클래스의 Static과 Instance의 차이점
- 클래스는 두 가지 타입 : 스태틱 타입, 인스턴스 타입
interface ClockConstructor {
new (hour: number, minute: number); // Construct signature
}
// Error! 인터페이스를 Implements할 때 클래스의 Instance적인 면만 검사.
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
- 생성 시그니처 (construct signature)로 인터페이스를 생성하고, 클래스를 생성하려고 한다면, 인터페이스를 implements 할 때, 에러가 발생하는 것을 확인할 수 있다
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);
- ClockConstructor는 생성자를 정의하고, ClockInterface는 인스턴스 메서드를 정의하는 두 인터페이스를 정의합니다. 그리고, 편의를 위해, 전달된 타입의 인스턴스를 생성하는 createClock 생성자 함수를 정의합니다
- createClock의 첫 번째 매개변수는 createClock(AnalogClock, 7, 32)안에 ClockConstructor 타입이므로, AnalogClock이 올바른 생성자 시그니처를 갖고 있는지 검사합니다.
interface ClockConstructor {
new (hour: number, minute: number);
}
interface ClockInterface {
tick();
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}
- 클래스 표현을 사용하는 것도 간단한 방법
8) 인터페이스 확장 (Extending Interfaces)
- 인터페이스는 서로를 extends 가능
- 인터페이스 자체를 재사용 가능한 구성요소로 분리하여 유연하게 사용
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
// 여러 인터페이스 확장
interface Square extends Shape, PenStroke {
sideLength: number;
}
// let square = <Square>{};
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
9) 하이브리드 타입 (Hybrid Types)
- 함수이기도 하면서, 객체이기도 한 인터페이스가 존재할 수 있음
- ex) jQuery의 $
- 호출 시그니쳐와 속성 타입 정의가 동시에 가능
interface jQueryElement {
// ...
}
interface jQueryInterface {
(string: query): jQueryElement;
each: Function;
ajax: Function;
// ...
}
const listItem = $('.list-item')
$.each(listItem, (index, item) => {...})
10) 클래스 확장 인터페이스
- 인터페이스 타입이 클래스 타입을 확장하면, 클래스의 멤버는 상속받지만 구현은 상속받지 않는다 (= 클래스 멤버 모두를 선언한 것과 마찬가지)
- 인터페이스는 기초 클래스의 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 {
private state: any;
select() {}
}
class Location {
}