인터페이스(interface)

Donggu(oo)·2023년 6월 27일

TypeScript

목록 보기
8/8
post-thumbnail

1. 인터페이스란?


  • 인터페이스(interface)는 객체, 함수, 클래스의 구조를 표현하는 약속이며 일반적으로 변수, 함수, 클래스에 타입 체크를 위해 사용된다.

  • 인터페이스를 이용해 객체와 함수가 지정된 형태를 갖도록 규정하고 통제할 수 있으며, 인터페이스를 지정하는 방법은 객체에 대한 새로운 타입을 생성하는 방법과 유사하다.

  • 직접 인스턴스를 생성할 수 없고 모든 메서드가 추상 메서드이다. 추상 클래스의 추상 메서드와 달리 abstract 키워드는 사용할 수 없다.

  • 아래 예제를 보면 인터페이스를 인자로 받아 사용할 때 항상 인터페이스의 속성 갯수와 인자로 받는 객체의 속성 갯수를 일치시키지 않아도 되는 것을 확인할 수 있다. 즉, 인터페이스에 정의된 속성, 타입의 조건만 만족한다면 객체의 속성 갯수가 더 많아도 상관 없다는 뜻이다. 또한, 인터페이스에 선언된 속성 순서를 지키지 않아도 된다.

  • 단, 정의한 프로퍼티 값을 누락하거나 정의하지 않는 값을 인수로 전달 시 컴파일 에러가 발생한다.

function logAge(obj : { age : number }) {
  console.log(obj.age);  // 28
}

let person = { name : 'Capt', age : 28 };

logAge(person);
// 위 코드를 interface로 변환
interface Person {
  age: number;
}

function logAge(obj : Person) {
  console.log(obj.age);  // 28
}

let person = { name : 'Capt', age : 28 };

logAge(person);

2. 옵션 속성


  • 인터페이스를 사용할 때 인터페이스에 정의되어 있는 속성 또는 메서드를 반드시 사용하지 않고, 필요에 따라 선택적으로 사용할 수 있다. 이 경우 옵션(Optional) 속성 설정을 통해 사용자가 선택적으로 사용하게 설정할 수 있다.

  • 속성 이름 뒤에 ?를 붙이면 옵션 속성이 되며, 이 옵션을 선택적으로 사용할 수 있게 된다.

  • 아래 예제를 보면 brewBeer() 함수에서 Beer 인터페이스를 인자의 타입으로 선언했음에도 불구하고, 인자로 넘긴 객체에는 hop 속성이 없다. 왜냐하면 hop을 옵션 속성으로 선언했기 때문이다.

interface CraftBeer {
  name: string;
  hop?: number;  
}

function brewBeer(beer : CraftBeer) {
  console.log(beer.name);  // Saporo
}

let myBeer = { name : 'Saporo' };
brewBeer(myBeer);

3. 읽기 전용 속성


  • 읽기 전용 속성은 인터페이스로 객체를 처음 생성할 때만 값을 할당하고 그 이후에는 변경할 수 없는 속성을 의미한다.

  • 속성 이름 앞에 readonly를 넣어 읽기 전용 속성으로 설정할 수 있다.

interface CraftBeer {
  readonly brand: string;
}

let myBeer : CraftBeer = { brand: 'Belgian Monk' };
// 인터페이스로 객체를 선언하고 나서 수정하려고 하면 오류가 난다.
myBeer.brand = 'Korean Carpenter';  // error!
  • 배열을 선언할 때 ReadonlyArray<T> 타입을 사용하면 읽기 전용 배열을 생성할 수 있다. 아래의 배열을 ReadonlyArray로 선언하면 배열의 내용을 변경할 수 없다. (선언하는 시점에만 값을 정의할 수 있다.)
let arr : ReadonlyArray<number> = [1,2,3];
arr.splice(0,1);  // error
arr.push(4);  // error
arr[0] = 100;  // error

4. 인터페이스 병합과 확장


1) 병합

  • 인터페이스를 같은 이름으로 중복 정의하면 인터페이스에 선언된 속성들은 모두 병합되어 하나의 인터페이스에 쓰인 것과 같아진다.

  • 아래 예제에서는 IPerson이라는 인터페이스가 2개 있다. name 속성은 중복이지만 agetel은 중복되지 않는다. 이 속성들은 병합되어 IPersonname, tel, age 속성을 정의한 인터페이스로 작동한다.

  interface IPerson {
    name: string;
    age: number;
  }

  interface IPerson {
    name: string;
    tel: string;
  }

  let p5: IPerson = {
    name: "홍길동",
    tel: "010-111-2222",
    age: 20,
  };

2) 확장

  • 인터페이스는 타입(type)과 유사해 보이지만 확장하는 방법이 서로 다르다. 일반적인 타입은 인터섹션(&)을 이용해 확장하고, 인터페이스는 상속(extends)을 이용해 확장한다.
// IEmployee 인터페이스에서 Iperson2 인터페이스를 확장하고 있다.
interface IPerson2 {
  name: string;
  age: number;
}

interface IEmployee extends IPerson2 {
  employeeId: string;
  dept: string;
}

let e1: IEmployee = {
  employeeId: "E001",
  dept: "회계팀",
  name: "홍길동",
  age: 20,
};

5. interface와 type의 차이점


1) 확장성과 선언 병합

1-1. interface

  • interface는 선언적 병합(declaration merging)을 지원한다. 같은 이름으로 여러 번 선언하면 자동으로 병합되어 기능을 확장할 수 있다.
interface User {
  name: string;
}

// User 인터페이스를 두 번 선언했지만, name과 age가 모두 있는 단일 User 타입으로 병합된다.
interface User {
  age: number;
}

const user: User = {
  name: "Alice",
  age: 30,
};
  • extends 키워드를 통해 다른 인터페이스를 상속받아 확장할 수 있다.
interface Person {
  name: string;
}

// Employee는 Person을 상속받아 name에 더해 employeeId를 가진다.
interface Employee extends Person {
  employeeId: number;
}

const employee: Employee = {
  name: "Bob",
  employeeId: 1234,
};

1-2. type

  • type은 일단 선언되면 재선언이나 병합이 불가능하다. 동일 이름으로 다시 선언하면 컴파일 에러가 발생한다.
type Product = {
  id: number;
  title: string;
};

// type Product는 한 번만 선언 가능하며 같은 이름으로 다시 선언하면 에러가 발생한다.
// Duplicate identifier 'Product'.
// type Product = {
//   price: number;
// };
  • & 연산자를 통해 교차 타입(intersection type)으로 확장할 수 있지만, 인터페이스처럼 선언적 병합은 지원하지 않는다.
type ProductBase = {
  id: number;
  title: string;
};

type ProductWithPrice = ProductBase & {
  price: number;
};

const p: ProductWithPrice = {
  id: 1,
  title: "Notebook",
  price: 9.99,
};

2) 유니언·튜플·원시 타입 지원 여부

2-1. interface

  • interface는 오직 객체 형태만 정의할 수 있기 때문에, 유니언 타입이나 튜플, 원시 타입을 직접 정의할 수 없다. interface는 객체의 구조를 정의하고, 그 안에 포함된 속성만 명시할 수 있다.
// 유니언 타입을 interface로 선언할 수 없다.
interface NameOrID = { name: string } | { id: number };

2-2. type

  • type은 유니언(union), 튜플(tuple), 원시 타입(alias)까지 모두 정의할 수 있어 복합 타입 구성에 강하다.

  • 예를 들어, ID라는 타입을 정의할 때, number 또는 string 타입을 받을 수 있게 하여 유연성을 제공할 수 있다. 또한, 튜플이나 조건에 맞는 원시 타입을 포함하는 타입을 정의할 수 있다.

type ID = number | string;  // 유니언 타입
type Point = [number, number];  // 튜플
type NameOrID = { name: string } | { id: number };  // 유니언 타입

const a: ID = 42;
const b: ID = "xyz";
const pt: Point = [10, 20];

3) 조건부·매핑 타입 지원 여부

3-1. interface

  • interface는 객체 형태의 구조만 정의하기 때문에 조건부 타입(conditional type)이나 매핑된 타입(mapped type)을 직접 선언할 수 없다.
// interface로는 조건부 타입을 선언할 수 없다.
// interface ElementType<T> = T extends (infer U)[] ? U : T;

// interface로는 매핑된 타입을 선언할 수 없다.
// interface ReadonlyProps<T> {
//   readonly [P in keyof T]: T[P];
// }

3-2. type

  • type은 조건부 타입과 매핑된 타입을 모두 정의할 수 있어 복잡한 타입 연산과 유틸리티 타입 작성에 강하다.
// 1) 조건부 타입 (Conditional Type)
type ElementType<T> = T extends (infer U)[] ? U : T;

type A = ElementType<string[]>;  // string
type B = ElementType<number>;    // number

// 2) 매핑된 타입 (Mapped Type)
// 모든 프로퍼티를 읽기 전용으로 바꾸는 ReadonlyProps
type ReadonlyProps<T> = { readonly [P in keyof T]: T[P] };
// 모든 프로퍼티를 선택적(optional)으로 바꾸는 PartialProps
type PartialProps<T>  = { [P in keyof T]?: T[P] };
// K 집합에 대해 동일한 타입 T를 매핑하는 Record 타입
type MyRecord<K extends keyof any, T> = { [P in K]: T };

interface User { id: number; name: string; }

// ReadonlyProps 예시
type ReadonlyUser = ReadonlyProps<User>;  
// { readonly id: number; readonly name: string; }

// PartialProps 예시
type PartialUser = PartialProps<User>;    
// { id?: number; name?: string; }

// Record 예시
type RoleMap = MyRecord<'admin' | 'user', User>;
// { admin: User; user: User; }
profile
FE Developer

0개의 댓글