타입스크립트 인터페이스 심화

박찬영·2023년 11월 27일

인터페이스 심화

우리가 인터페이스를 사용하려면 기본적인 작동 방법 말고도
심화적으로 들어가면 더 다양하게 코드를 구성하고 유지보수 하기 쉬워진다.

기본 설명

프로그래밍에서 클래스 또는 함수의 '틀' 을 정의하는 것처럼,
타입의 '틀'로서 사용할 수 있는 것이 인터페이스 인 것이다.
가장 큰 장점은 재사용성이 좋다는 것이다.

밑에 코드는 인터페이스의 예시 이다.

// 인터페이스명은 대문자로 짓는다
interface Human {
  name: string; 
  age: number; 
  boo(): void; 
}

// 인터페이스 자체를 타입으로 줘서 객체 생성
const person: Human = {
  name: "da",
  age: 5,
  boo: () => console.log("this is boo"),
};

// 매개변수에서 인터페이스를 타입으로 받는다.
function booboo(a: Human): void {
  console.log(`${a.name} is ${a.age} years old`);
};

booboo(person); // da is 5 years old
person.boo(); // this is boo

보통 인터페이스명을 명명할때 앞에다가 항상 대문자 I 를 붙이는 암묵적인 약속이 행하여 왔다.
위의 코드를 를 들면 Human이 아니라 IHuman 이런식으로 말이다.
왜냐하면 첫글자에 대문자 I를 붙여 이 단어는 인터페이스임을 쉽게 알아볼수 있기 때문이었다.
실제로 C#을 하다 오신 개발자분들은 C#에 익숙하여 대문자 I 를 붙이기도 한다.
하지만 요즘 들어서는 대문자 I 를 빼는 추세로 되바뀌었다.
왜냐하면 어차피 에디터에서 마우스 커서를 대면 타입 구조가 나오기 때문에
굳이 가독에 안좋게 뭣하러 앞에 붙이냐는 의견에 대세가 기울었기 때문이다.


심화 들어가기

인터페이스 VS 타입 별칭

인터페이스와 타입 별칭의 기능은 비슷한데 어떤걸 쓰는게 좋을까?
타입스크립트 공식 문서에서도 가능하다면 인터페이스를 사용하는 것을 추천한다.
또한 인터페이스는 타입 별칭은 할 수 없는 확장이라는 기능이 가능하기 때문이다.
인터페이스는 단순히 객체 타입을 표현을 떠나 다양하게 활용 될 수 있으며 지원하는 문법 또한 다양하다.

물론 type 별칭도 타입스크립트 2.7 버전부터 교차 타입을 생성함으로써 extend 할 수 있다.
예를 들어, type Cat = { nyan: false } 와 type Bird =  { purrs: true } 가 있으면
type Fusion = Cat & Bird 이런식으로 and로 묶으면 이는 곧 확장이 되기 때문이다.

그래도 가능하다면? 타입 별칭보다 인터페이스를 사용해야 하는 것이 옳은 측면으로 볼 수 있다.

선택적 프로퍼티

인터페이스를 사용하다 인터페이스에 정의되어 있는 속성을 무조건 사용해야 한다면
코드의 유연성이 사라지는 모습을 볼 수 있다.
선택적 프로퍼티는 인터페이스의 코드의 유연성을 추가해주는 좋은 기능이라고 생각하면 된다.

interface User {
	name: string;
	age?: number;
}

const userInfo = (user: User) => {
	return `${user.name}${user.age ?? 0 } 살 입니다.`
}

userInfo({name: 'chan'})

근데 여기서 주의할 점은 age는 선택 속성이기 때문에 다음과 같이 age 속성을 사용하는 로직을 짜게 되면,
컴파일러는 age 속성이 확실하게 쓰이는지 안쓰이는지 추론을 할수 없기 때문에 에러를 내뿜게 된다.

interface User {
	name: string;
	age?: number;
}

const userInfo = (user: User) => {
	if(user.age < 19) `${user.name} 은 성인입니다.`
	return `${user.name}${user.age ?? 0 } 살 입니다.`
}

userInfo({name: 'chan'}) // 오류

이럴때는 타입 가드 라는 기법을 사용하면 된다. user.age 속성이 존재할때 조건식을 비교하라고 짜면
age 속성을 확실하게 쓸 수 있다고 컴파일러가 생각하기 때문에 오류가 생기지 않는다.

if(user.age && user.age < 19) `${user.name} 은 성인입니다.`

읽기 전용 프로퍼티

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

interface User {
   name: string;
   age: number;
   gender?: string;
   readonly birthYear: number; // 읽기 전용 속성
}

let user: User = {
   name: 'chan',
   age: 26,
   birthYear: 1998, // 최초에 값을 초기화 할때만 할당이 가능
};

user.birthYear = 1999; // Error - 이후에는 수정이 불가능

만약 모든 인터페이스에 readonly 를 쓰고 싶다면 하나하나 다 입력을 해야 할까?
그렇게 하면 코드를 볼때 이상해 보일 수가 있다 따라서 밑에 방법중에 한가지를 쓰면 된다.

// Readonly 유틸리티(Utility) 활용
interface IUser {
  name: string,
  age: number
}

let user: Readonly<IUser> = { // Array 처럼 따로 Readonly 라는 자료형이 있다고 생각하면 된다
  name: 'Neo',
  age: 36
};

user.age = 85; // Error
user.name = 'Evan'; // Error
// Type assertion
let user = {
  name: 'Neo',
  age: 36
} as const; 
// 따로 인터페이스 말고 객체 데이터 자체에 as const를 붙이게 되면 이 자체가 리터럴 타입이 되게 된다.

user.age = 85; // Error
user.name = 'Evan'; // Error

배열도 사용할 수가 있다.

let arr: ReadonlyArray<number> = [1,2,3]; // 읽기 전용 배열

arr.splice(0,1); // error
arr.push(4); // error
arr[0] = 100; // error

인터페이스 호환

만약 같은 이름의 인터페이스를 여러 개 만들 수도 있으면 어떻게 작동할까?

이들은 중복이 되면, 안의 선언된 프로퍼티가 하나로 합쳐진다고 보면된다.
기존에 만든 인터페이스에 무엇인가 추가하고 싶은 내용이 있는 경우 자주 쓰이는 기법이다.

interface IFullName {
  firstName: string,
  lastName: string
}

interface IFullName {
  middleName: string
}

const fullName: IFullName = {
  firstName: 'Tomas',
  middleName: 'Sean',
  lastName: 'Connery'
};

인터페이스 확장

인터페이스 호환과 기능이 비슷한 인터페이스 확장이 있다.

interface Person {
   name: string;
}

interface Developer extends Person {
   skill: string;
}

let fe: Developer = { name: 'josh', skill: 'TypeScript' };

또한 인터페이스 확장은 여러개를 한번에 확장 할 수 있다.

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

interface Programmer {
   favoriteProgrammingLanguage: string;
}

interface Korean extends Person, Programmer { // 두개의 인터페이스를 받아 확장
   isLiveInSeoul: boolean;
}

const person: Korean = {
   name: '홍길동',
   age: 33,
   favoriteProgrammingLanguage: 'kor',
   isLiveInSeoul: true,
};

인터페이스 함수 타입

인터페이스는 객체를 정의할 때 말고도 함수를 호출할때도 사용할 수 있다.
인터페이스도 함수의 모양을 정의할 수 있는데 전문용어로 호출 시그니처 라고도 한다.

interface login {
  (username: string, password: string): boolean;
}

let loginUser: login = function(id, pw) {
  console.log('로그인 했습니다');
  return true;
}

여기서 중요한 것은 매개 변수 이름이 인터페이스와 일치할 필요가 없다.
또한 타입 추론을 통해 선언할 함수에 타입을 굳이 쓸 필요가 없다.

인터페이스 함수 오버로드

인터페이스 내에서도 함수 자체 타입을 오버로드 시켜 멀티 함수를 구현할 수 있다.

interface Add {
  (x: number, y: number): number;
  (x: string, y: string): string;
}
const add: Add = (x: any, y: any) => x + y;

인터페이스 클래스 타입

인터페이스로 클래스를 정의하는 경우, implements 키워드를 사용해 클래스 정의 옆에다 붙여주면 된다.

interface IUser {
   name: string;
   getName(): string;
}

// IUser 인터페이스를 implements 하면 User 클래스의 프로퍼티 구조는 
// 반드시 IUser에 정의된 대로 따라야 한다.
// 즉, 반드시 name 변수와 getName() 메소드를 클래스에 기본값으로 구현해야 한다.
class User implements IUser {
   name: string;

   constructor(name: string) {
      this.name = name;
   }

   getName() {
      return this.name;
   }
}

const neo = new User('Neo');
neo.getName(); // Neo

인덱스 타입

지금까지 사용한 인터페이스는 직접 속성의 타입을 하나하나 지정해주어 사용하였다.

하지만 인터페이스에서 정의할 속성들이 엄청 많아 질경우 애로사항이 생길수 있는데,
이때 규칙이 있는 속성이라면, 우리가 마치 변수라는 곳에 값을 넣어 유기적으로 이용하듯이,
인터페이스도 이를 활용할 수 있다.

인덱서의 타입은 string과 number만 지정할 수 있다는 점에 유의하자.
이는 생각해보면 당연한 원리인데, key는 문자만 되고(object.key),
배열은 인덱스(array[0])는 숫자이기 때문이다.

type Score = 'A' | 'B' | 'C' | 'D' | 'F';

interface User {
   name: string;
   [grade: number]: Score;
}

const user1: User = {
   name: '홍길동',
   1: 'A',
};

const user2: User = {
   name: '임꺾정',
   3: 'F',
};

const user3: User = {
   name: '박혁거세',
   2: 'B',
};
interface IItem {
  [itemIndex: number]: string // Index signature
}

let item: IItem = ['a', 'b', 'c']; // Indexable type

console.log(item[0]); 
console.log(item[1]); 
console.log(item['0']); // Error - itemIndex 인덱서블 속성은 number 타입이라 에러
interface IItem {
  [itemIndex: number]: string | boolean | number[] // 여러개의 타입을 유니온
}

let item: IItem = ['Hello', false, [1, 2, 3]];

console.log(item[0]); // Hello
console.log(item[1]); // false
console.log(item[2]); // [1, 2, 3]
profile
오류는 도전, 코드는 예술

0개의 댓글