프로그래밍/Frontend

[TypeScript] Type + JavaScript - (2) Interface

Churnobyl 2024. 7. 27. 14:05
728x90
반응형


1. 타입 호환성 (Type Compatibility, Type Equivalence)

  타입 호환성은 프로그래밍 언어론(Programming Language Theory)에서 언급되는 내용으로 어떤 맥락에서 타입 A를 가진 객체가 타입 B와 동등한지(취급해도 되는지)를 판단하는 것이다. 만약 A타입을 가진 객체를 B로 취급해도 된다면 A를 B로 할당 가능하다고 하며, 타입 A는 타입 B의 서브타입이라고 한다. 그리고 프로그래밍 언어마다 이러한 타입 호환성의 정의 범위는 다르다.

 

 예컨데 C#이나 자바 등의 언어에서 '사람'이라는 클래스를 확장한 '학생'이라는 클래스가 있을 경우 '학생'클래스는 '사람' 클래스로 취급할 수 있으며 '학생'클래스는 '사람'클래스의 서브타입이다. 하지만 '강아지'라는 클래스가 '사람' 클래스를 확장하지 않았다면 '강아지'클래스는 '사람'클래스로 취급할 수 없다. 이렇게 '학생클래스는 사람클래스를 확장한 클래스다!' 라고 타입을 구체적으로 명시해 타입 호환성을 판단하는 것을 명목적 타이핑 체계라고 한다.

 


2. 명목적 타이핑 vs 구조적 타이핑

출처 : https://speakerdeck.com/takasek/what-is-a-nominal-type

 

 위에서 언급한 것처럼 명목적 타이핑 체계를 사용하는 언어는 C#이나 자바같은 언어다. 타입 호환성 체계에는 명목적 타이핑 체계(Nominal Type System)과 구조적 타이핑 체계(Structural Type System)으로 구분할 수 있다. 명목적 타이핑은 위에서 알아봤고 구조적 서브타이핑에 대해 알아보자. 구조적 서브타이핑은 타입의 관계를 명시적으로 정의하는 명목적 타이핑 체계와는 다르게 A타입과 B타입이 같은 형태를 가지고 있는지 아닌지가 판단의 중점이다.

 

구조적 타이핑 체계 (덕 타이핑)

 

 즉, '오리 같이 생김', '수영함', '꽥꽥거림'라는 속성을 가진 물체가 존재한다면 우리는 확신할 수는 없지만 이 물체를 오리라고 판단할 수 있는 것과 같은 맥락이다. 만약 명목적 타이핑 체계를 사용한다면 '오리'라는 물체는 '오리같이 생겼고 수영을 하며 꽥꽥거린다'라고 딱 규정을 해놓고 여기에 부합하는 물체만 오리라고 판단할 것이다. 결론적으로 구조적으로 같은 속성을 가지고 있다면 호환 가능하다고 판단하는 유연한 체계를 구조적 타이핑 체계라고 하며 다른 말로는 덕 타이핑(Duck Typing)이라고도 한다.

 

 


 3. 인터페이스 (Interface)

  타입스크립트로 돌아와서 타입스크립트의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞춘다는 것이다. 이는 위에서 설명한 구조적 서브타이핑(Structural Subtyping) 혹은 덕 타이핑이다. 타입의 엄격한 검사를 통해 높은 코드 품질을 지향하는 타입스크립트지만, 함수 표현식이나 객체 리터럴같은 익명 객체를 광범위하게 사용하는 자바스크립트 코드의 작성 방식을 자연스럽게 표현하기 위해서는 구조적 타이핑 시스템을 사용하는 것이 좋다고 판단한 것으로 보인다.

 

 타입스크립트에서는 속성들을 모아놓은 타입들의 이름을 짓기 위해서 인터페이스를 사용한다. 인터페이스는 앞서 공부한 원시타입처럼 변수, 함수, 클래스에 전부 사용할 수 있으며, 함수가 리턴하는 값에 대해서도 기술할 수 있다. 인터페이스는 쉽게 생각해서 원시타입 하나로 표현할 수 없는 복합적인 속성들로 이루어진 타입의 이름을 지어주는 것이다.

 

// id, content, completed 속성으로 이루어진 타입을 Todo라고 할 것이다.
interface Todo {
  id: number;
  content: string;
  completed: boolean;
}

// todos 변수는 Todo의 배열이다.
let todos: Todo[];

// todos 변수는 Todo의 배열로 정의되어 있으니까 요구사항에 맞게 넣어줘야지
todos = [
  {
    id: 1,
    content: "타입스크립트 공부하기",
    completed: false,
  },
  {
    id: 2,
    content: "Todo앱 만들기",
    // Error : Property 'completed' is missing in type '{ id: number; content: string; }' but required in type 'Todo'.ts(2741)
  },
  {
    id: 3,
    content: "리액트 프로젝트 만들기",
    completed: false,
    expireDate: "5월 5일",
    // Error: Object literal may only specify known properties, and 'expireDate' does not exist in type 'Todo'.ts(2353)
  },
];

 

 위의 코드에서 Todo 인터페이스를 선언해 주었다. Todo 인터페이스는 number타입 id, string타입 content, boolean타입 completed로 이루어져 있다. 그 다음으로 todos변수에 Todo[] 인터페이스를 타입으로 선언했으므로 이제 todos변수는 해당 인터페이스를 준수해야 한다.

 

 그리고 타입 검사는 속성들의 순서를 지킬 필요가 없다. 단지 인터페이스가 요구하는 속성들이 존재하는지, 속성들이 요구하는 타입을 가졌는지만 판단한다.

 

 

선택적 프로퍼티 (Optional Properties)

 인터페이스의 모든 속성이 항상 전부 필요한 것이 아니라면 선택적 프로퍼티를 사용할 수 있다.

 

interface Todo {
  id: number;
  content: string;
  completed: boolean;
  expireDate?: Date;
}

let todos: Todo[];

todos = [
  {
    id: 1,
    content: "타입스크립트 공부하기",
    completed: false,
  },
  {
    id: 2,
    content: "Todo앱 만들기",
    completed: false,
    expireDateeeee: new Date(),
    // Error: Object literal may only specify known properties, but 'expireDateeeee' does not exist in type 'Todo'. Did you mean to write 'expireDate'?ts(2561)
  },
  {
    id: 3,
    content: "리액트 프로젝트 만들기",
    completed: false,
    expireDate: new Date(),
    // OK
  },
  {
    id: 4,
    content: "개인 프로젝트 완성하기",
    completed: false,
    // OK
  },
];

 

 Todo 인터페이스의 expireDate 속성 끝에 ? 를 붙여 표시하면 선택적 프로퍼티가 된다. 선택적 프로퍼티의 이점은 id:2 객체의 expireDateeeee처럼 인터페이스에 속하지 않는 프로퍼티의 사용을 방지하면서, 사용 가능한 속성을 기술하는 것이다. 따라서 id:3, id:4의 객체 둘 다 Todo 인터페이스로 취급된다.

 

 

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

 어떤 속성은 객체가 처음 생성될 때만 수정 가능하고 이후에는 변경되는 것을 방지해야 한다. 이러한 경우 프로퍼티 이름 앞에 readonly를 붙여서 읽기전용 프로퍼티를 지정할 수 있다. 그럼 읽기전용 프로퍼티로 지정된 프로퍼티는 처음 할당 후 바꿀 수 없는 const처럼 행동한다.

 

interface CustomSquare {
  readonly X: number;
  readonly Y: number;
  edge: number;
}

let square: CustomSquare;

square = {
  X: 5,
  Y: 2,
  edge: 10,
};

console.log(
  "x : ",
  square.X,
  " y : ",
  square.Y,
  " area: ",
  square.edge * square.edge
);
// x :  5  y :  2  area:  100

square.X = 100;
// Error: Cannot assign to 'x' because it is a read-only property.

square.edge = 50;
// OK

 

 

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

 JavaScript에서는 어떤 함수에 객체 리터럴이 잘못 전달되면 에러를 알려주지 않고 이상한 결과만을 뱉는다. 아래의 간단한 예제를 보자.

 

function add({ a, b, c }) {
  console.log(c);
  // undefined;
  return a + b + c;
}

console.log(add({ a: 5, b: 5, d: 5 }));
// NaN

 

 자바스크립트에서 a, b, c로 이루어진 객체 타입을 파라미터로 받는 add함수에 a, b, d로 이루어진 객체 타입을 넘긴다면 c에 대해 어떠한 값도 전달되지 않았기 때문에 c는 undefined값을 갖는다. 결과적으로 a + b + c연산은 5 + 5+ undefined가 되기 때문에 Not a Number 즉, 숫자가 아니다라는 결과를 얻는다.

 

하지만 타입스크립트는 이러한 코드가 버그가 있을 수 있다고 생각한다. 타입스크립트에서 객체 리터럴 즉 object 리터럴은 (1)다른 변수에 할당될 때나 (2)인수로 전달될 때 초과 프로퍼티 검사를 받는다. 만약 객체 리터럴이 대상 타입이 가지고 있지 않은 프로퍼티를 갖고 있다면 에러가 발생한다.

 

function add({ a, b, c }) {
  return a + b + c;
}

console.log(add({ a: 5, b: 5, d: 5 }));
// Error: Object literal may only specify known properties, and 'd' does not exist in type '{ a: any; b: any; c: any; }'.ts(2353)

 

 에러의 뜻은 다음과 같다.

객체 리터럴은 알려진 프로퍼티들로만 지정할 수 있습니다. 그리고 'd'은 {a: any; b: any;c: any; }타입에 존재하지 않습니다. ts(2353)

 

 

반응형