TypeScript 2 : Interfaces

백은진·2020년 11월 29일
2

TIL (Today I Learned)

목록 보기
54/106

Interfaces

TypeScript는 값의 '형태'에 초점을 맞추어 타입을 검사한다. 이런 점을 Duck typing(덕 타이핑) 혹은 Structural subtyping(구조적 서브타이핑)이라고 한다.
Interface는 이런 타입들의 이름을 짓는 역할을 한다. 더불어 코드 안의 계약을 정의하고, 프로젝트 외부에서 사용하는 코드의 계약을 정의한다.

첫 번째 인터페이스 (Our First Interface)

예제 1)

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

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

타입 검사는 printLabel 호출을 확인한다. printLabel 함수는 string 타입의 lable을 갖는 객체를 하나의 매개변수로 가진다.
(이 객체가 실제로는 더 많은 프로퍼티를 갖고 있으나, 컴파일러는 최소한으로 필요한 프로퍼티가 있는지와 타입이 잘 맞는지만 검사한다.)

예제 2)

interface LabeledValue {
    label: string;
}

function printLabel(labeledObj: LabeledValue) {
    console.log(labeledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

예제 1과 예제 2는 같은 경우이다.
다만, 예제 1에서는 일반 함수를 이용해 타입을 검사했고, 예제 2에서는 interface를 이용해 타입을 검사한 점이 다르다.

타입 검사는 프로퍼티들의 순서를 요구하지 않는다. 단지 인터페이스가 요구하는 프로퍼티들이 존재하는지와 프로퍼티들이 요구하는 타입을 가졌는지만을 확인한다.

선택적 프로퍼티 (Optional Properties)

프로퍼티는 어떤 조건에서만 존재하거나 아예 없을 수 있다. 이를 선택적 프로퍼티라고 하며, 객체 안의 몇 개의 프로퍼티만 채워 함수에 전달하는 option bags같은 패턴을 만들 때 선택적 프로퍼티가 유용하다.

선택적 프로퍼티를 가지는 인터페이스는 다른 인터페이스와 비슷하게 작성되나, 선언에서 프로퍼티 네임 끝에 물음표 ?를 붙여 표시한다.

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

선택적 프로퍼티 장점

1. 인터페이스에 속하지 않는 프로퍼티의 사용 방지 (프로퍼티 이름 잘못 입력 시, 오류 메세지로 알림)

2. 사용 가능한 속성 기술

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

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

let mySquare = createSquare({color: "black"});

읽기전용 프로퍼티 (Readonly properties)

일부 프로퍼티들은 객체가 처음 생성될 때만 수정이 가능해야 한다. 이 때, 프로퍼티 이름 앞에 readonly를 넣어서 이를 지정할 수 있다.

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

객체 리터럴을 할당하여 객체를 생성하며, 할당 후에는 값을 수정할 수 없다.

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // 오류!

TypeScript에서는 모든 변경 메소드(Mutating Methods)가 제거된 Array<T>와 동일한 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; // 오류! ReadonlyArray를 일반 배열에 재할당할 수 없다. 

a = ro as number[]; // 타입 단언으로 오버라이드하는 것은 가능하다. 

readonly vs const

변수는 const를 사용하고, 프로퍼티는 readonly를 사용한다.


초과 프로퍼티 검사 (Excess Property Checks)

첫 번째 인터페이스, 선택적 프로퍼티, option bags를 그냥 결합하면 에러가 발생할 수 있다.

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

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

let mySquare = createSquare({ colour: "red", width: 100 }); // 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({ width: 100, opacity: 0.5 } as SquareConfig);

문자열 인덱스 서명(string index signatuer)

추가 프로퍼티가 있는 것이 확실할 때는, 문자열 인덱스 서명을 추가하는 것이 더 좋다.

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

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

그러나, 초과 프로퍼티 에러의 대부분은 실제 버그이기 때문에, 검사를 피하는 방법은 시도하지 않는 것이 좋다.

예를 들어, 만약 createSquare에 color나 colour 모두 전달해도 괜찮다면, squareConfig가 이를 반영하도록 정의를 수정해야 한다.

함수 타입 (Function Types)

인터페이스는 JavaScript 객체가 가질 수 있는 넓은 범위의 형태를 기술할 수 있다. 프로퍼티로 객체를 기술하는 것 외에, 인터페이스는 함수 타입을 설명할 수 있다.

인터페이스로 함수 타입을 기술하기 위해, 인터페이스에 호출 서명(call signature)을 전달해야 한다. 각 매개변수는 이름과 타입이 모두 필요하다.

interface SearchFunc {
    (source: string, subString: string): boolean;
}

한 번 정의되면, 함수 타입 인터페이스는 다른 인터페이스처럼 사용할 수 있다. 여기서 함수 타입의 변수를 만들고, 같은 타입의 함수 값으로 할당하는 방법을 보여준다.

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

함수 매개변수는 같은 위치에 대응되는 매개변수끼리 한 번에 하나씩 검사한다. 만약 타입을 전혀 지정하지 않고 싶다면, SearchFunc 타입의 변수로 직접 함수 값이 할당되었기 때문에 TypeScript의 문맥상 타이핑(contextual typing)이 인수 타입을 추론할 수 있다.

아래의 예제는 함수 표현의 반환타입이 반환하는 값으로 추론된다. (boolean) 따라서, 함수 표현식이 number나 string을 반환했다면, 타입 검사는 반환 타입이 인터페이스에 정의된 반환 타입과 일치하지 않는다는 에러를 발생시킨다.

let mySearch: SearchFunc;

// error: Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'.
// Type 'string' is not assignable to type 'boolean'.
mySearch = function(src, sub) {
  let result = src.search(sub);
  return "string";
};

인덱서블 타입 (Indexable Types)

a[10] 이나 ageMap["daniel"] 처럼 타입을 인덱스로 기술할 수 있다. 인덱서블 타입은 인덱싱을 할 때 해당 반환 유형과 함께 객체를 인덱싱하는 데 사용할 수 있는 타입을 기술하는 인덱스 시그니처를 가지고 있다.

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

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

StringArray 인터페이스의 인덱스 서명은 StringArray가 number로 indexed되면 string을 반환할 것을 나타낸다.

인덱스 서명을 지원하는 타입에는 문자열과 숫자가 있다.

두 타입의 인덱스 모두 지원이 가능하나, 숫자 인덱서에서 반환된 타입은 반드시 문자열 인덱스에서 반환된 타입의 하위 타입이어야 한다.

이유는 숫자로 인덱싱할 때, JavaScript는 실제로 객체를 인덱싱하기 전에 문자열로 변환하기 때문이다. 즉, 100 (number)으로 인덱싱하는 것은 "100" (string)으로 인덱싱하는 것과 같기 때문에 서로 일관성이 있어야 한다.

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// 오류: 숫자형 문자열로 인덱싱을 하면 완전히 다른 타입의 Animal을 얻게 될 것입니다!
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

문자열 인덱스 시그니처는 모든 프로퍼티들이 반환 타입과 일치하도록 강제한다. 문자열 인덱스가 obj.property / obj["property"] 모두 이용 가능함을 알려주기 때문이다.

interface NumberDictionary {
    [index: string]: number;
    length: number;    // 성공, length는 숫자입니다
    name: string;      // 오류, `name`의 타입은 인덱서의 하위타입이 아닙니다
}

그러나 인덱스 시그니처가 프로퍼티 타입들의 합집합이라면, 다른 타입의 프로퍼티들도 허용할 수 있다.

interface NumberOrStringDictionary {
    [index: string]: number | string;
    length: number;    // 성공, length는 숫자입니다
    name: string;      // 성공, name은 문자열입니다
}

인덱스의 할당을 막기 위해 인덱스 시그니처를 읽기 전용으로도 만들 수 있다.

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // 오류! 읽기 전용이기 때문에 myArray[2]의 값을 할당할 수 없다. 
profile
💡 Software Engineer - F.E

0개의 댓글