TypeScript의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞추고 있다는 거야. 이를 "duck typing" 또는 "structural subtyping" 이라고 해. 간단한 예시를 한번 보여줄게.
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);
인터페이스의 모든 프로퍼티가 필요한게 아니라 선택적으로 조건을 주고 싶을 때는 ?
를 쓰면 돼.
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
를 활용할 수 있어.
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; // 에러!
여기 마지막 줄에서 일반 배열인 a
에 ReadonlyArray
를 재할당 할 수 없는 걸 알 수 있겠지? 만약 오버라이드하고 싶으면 type assertion을 활용하면 돼. type assertion은 Basic Types 파트에서 배웠었어.
a = ro as number[];
**readonly
vs const
**
변수에는 const
를 쓰고 프로퍼티에는 readonly
를 사용하면 돼.
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는 대부분 실제 버그이기 때문에 웬만하면, 검사를 피하지 마. 매우 복잡한 객체 리터럴에서는 그럴 수 있지만, 만약 초과 프로퍼티 검사 문제가 발생하면 타입 정의를 수정하는게 더 좋은 방법이야. 예를 들어, createSquare
에 color
와 colour
모두 전달해도 괜찮으면 squareConfig
를 수정해줘야겠지?
함수의 매겨변수와 리턴 값의 타입을 정하는 방법이야. 마찬가지로 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이 자동으로 매개변수의 타입을 추론하는거지.
interface
로 인덱스의 타입을 기술할 수도 있어. 인덱싱 타입과 반환 값의 타입을 기술하는걸 index signature라고 해.
interface StringArray {
[index: number]: string; // number로 인덱싱하면 string을 반환한다
}
let myArray: StringArray;
myArray = ['BOB', 'FRED'];
let myStr = myArray[0];
console.log(myStr);
인덱싱에 지원되는 타입은 두 가지야. number
와 string
index signature는 프로퍼티의 반환 타입이 일치하도록 강제해.
interface NumberDitionary {
[index: string]: number;
length: number;
name: string; // 에러, 반환값이 string으로 맞지 않기때문이지
}
위의 예제를 허용하고 싶으면 |
를 사용하면 돼.
interface NumberDitionary {
[index: string]: number | string;
length: number;
name: string; // 에러, 반환값이 string으로 맞지 않기때문이지
}
클래스에서 인터페이스를 적용할 때 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
를 이해하기가 조금 까다로운데 생성자는 hour
와 minute
을 가지고 있으며 그렇게 생성된 인스턴스는 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;
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() {}
}
SelectableControl
은 Control
을 상속받고 있기 때문에 Control
혹은 Control
의 하위 클래스에 의해서만 구현될 수 있어. Button
, TextBox
가 이에 해당되겠지?
Image
클래스가 왜 안되냐면 Control
클래스에 의해 만들어진 state
여야만 하는데(private
이기 때문에) Image
클래스에서 새롭게 정의한 state
라서 호환이 안되는거야.