✍ 공부/TypeScript

[ 러닝 타입스크립트 ] 챕터 7. 인터페이스

Po_tta_tt0 2023. 3. 7. 22:58
반응형

 

 

타입스크립트 스터디에서 정리한 내용!
interface는 자주 사용해서 간단하겠거니~ 생각하고 읽기 시작했는데 내 생각보다 나는 더 모르고 타입의 세계는 넓다는 것을 깨달았다.

 

 

챕터7. 인터페이스

러닝 타입스크립트 131p

 

핵심

우리가 직접 만들 수 있는데
왜 지루한 내장 티입 형태만 사용하나요?

 

인터페이스? : 연관된 이름으로 객체 형태를 설명하는 또 다른 방법.

특징

  • 별칭으로 된 객체 타입과 유사
  • 더 읽기 쉬운 오류 메시지
  • 더 빠른 컴파일러 성능
  • 클래스와의 더 나은 상호 운용성
    을 위해 사용된다



📚 07.1 타입 별칭 vs. 인터페이스

// 타입 별칭
type Poet = {
  born: number;
  name: string;
}

// 인터페이스
interface Poet {
  born: number;
  name: string;
}

타입 별칭과 인터페이스는 거의 비슷해 보인다. 무슨 차이가 있을까?

 

타입 별칭과 인터페이스의 차이

  • 인터페이스는 속성 증가를 위해 병합할 수 있다. => 내장된 전역 인터페이스 또는 npm 패키지와 같은 외부 코드를 사용할 때 특히 유용하다
  • 인터페이스는 클래스가 선언된 구조의 타입을 확인하는 데 사용할 수 있다
  • 일반적으로 인터페이스에서 타입스크립트 타입 검사기가 더 빨리 작동한다.
    • 인터페이스 : 내부적으로 더 쉽게 캐시할 수 있는 명명된 타입 선언
    • 타입 별칭 : 새로운 객체 리터럴의 동적인 복사 붙여넣기
  • 인터페이스는 이름 없는 객체 리터럴의 별칭이 아닌 명명된 객체로 간주하므로, 특이케이스에서 나타나는 오류 메시지를 더 쉽게 읽을 수 있다

 

 

📚 07.2 속성 타입

 

자바스크립트 객체를 실제로 사용할 때 어려움을 느낄 수 있다.

타입스크립트 인터페이스는 이런 부분을 모델링할 수 있도록 유용한 타입 시스템 도구를 제공한다


  1. 선택적 속성
  2. 읽기 전용 속성
  3. 함수와 메서드
  4. 호출 시그니처
  5. 인덱스 시그니처
  6. 중첩 인터페이스



1. 선택적 속성

 

모든 객체가 필수적으로 인터페이스 속성을 가질 필요는 없다

=> 타입 애너테이션 앞에 '?'을 사용해 선택적 속성임을 나타낼 수 있다.

interface Apple {
  color?: string;
  name: string;
  price: number;
}

// 🤔 name : string, price : number, color은 string으로 들어오거나 생략할 수 있구나

const pickedApple = {
  name: 'green apple';
  price: 300;
} // 👍

 

2. 읽기 전용 속성

 

인터페이스에 정의된 객체의 속성을 재할당하지 못하도록 하고 싶다면?
속성 이름 앞에 readonly 키워드를 추가할 수 있다

 

interface Apple {
  color?: string;
  name: string;
  readonly price: number;
}

// 🤔 price는 재할당할 수 없구나!

function pickingApple(apple:Apple){
  console.log(apple.price) // ok
}

function changeApplePrice(apple:Apple){
  apple.price += 500 // ERROR!!
}

 

readonly 제한자

  • 타입 시스템에만 존재
  • 인터페이스에서만 사용 가능
  • 객체의 인터페이스를 선언하는 위치에서 사용 🌟
  • 실제 객체에는 적용되지 않는다

🌟 이게 무슨 말일까?

const newApple = {
  color: 'red',
  name: '맛있는 사과',
  price: 2000,
}

newApple.price += 500
// OK : newApple은 유추된 객체 타입이다 === Apple 객체가 아니기 때문에 가능!

changeApplePrice(newApple)
// OK : 유추된 객체 타입이었던 newApple의 더 구체적인 버전의 Apple을 읽는다

 

🙄 "언제 사용하면 좋을까?"

readonly는 단지 타입스크립트 타입 검사기를 사용해 개발 중에 그 속성이 수정되지 못하도록 보호하는 역할을 한다

 

3. 함수와 메서드

 

인터페이스 멤버를 함수로 선언하는 두 가지 방법

  • 메서드 구문: 객체의 멤버로 호출되는 함수로 선언

    member(): void
  • 속성 구문: 독립 함수와 동일하게 선언

    member: () => void

두 방법 모두 선택적 속성으로 만들 수 있다

interface OptionalReadonlyFunctions {
  a? : () => string; // 속성 구문
  b?(): => string; // 메서드 구문
}

메서드와 속성의 차이점

  • 메서드는 readonly로 선언할 수 없지만 속성은 가능하다
  • 인터페이스 병합은 메서드와 속성을 다르게 처리한다
  • 타입에서 수행되는 일부 작업은 메서드와 속성을 다르게 처리한다

 

두 가지를 혼동하거나 차이점을 이해하지 못한다고 해도 걱정하지 마세요
this 스코프와 선택한 방식을 의도하지 않는 한 코드에 거의 영향을 주지 않습니다

 

4. 호출 시그니처

인터페이스와 객체 타입은 호출 시그니처로 선언할 수 있다

호출 시그니처란?

  • 값을 함수처럼 호출하는 방식에 대한 타입 시스템의 설명이다
  • 할당 가능한 매개변수와 반환 타입을 가진 함수
  • 함수 타입과 비슷하지만 : 대신 =>로 표시한다
type FunctionAlias = (input: string) => number; 🌟

interface CallSignature {
  (input: string): number;
}

🌟🙄 "언제 사용하지?"

호출 시그니처는 사용자 정의 속성을 추가로 갖는 함수를 설명하는 데 사용할 수 있습니다

ex

interface FunctionWithCount {
    count : number;
    ():void
}

let hasCallCount : FunctionWithCount

function keepsTrackOfCalls(){
    keepsTrackOfCalls.count += 1;
    console.log(keepsTrackOfCalls.count)
}

keepsTrackOfCalls.count = 0;

hasCallCount = keepsTrackOfCalls;

function doseNotHaveCount(){
    console.log('no idea')
}

hasCallCount = doseNotHaveCount

 

5. 인덱스 시그니처

 

컨테이너 객체의 경우 모든 가능한 키에 대해 필드가 있는 인터페이스를 선언하는 것은 비현실적인거나 불가능합니다

타입스크립트는 인덱스 시그니처 구문을 제공해

  • 인터페이스의 객체가 임의의 키를 받고
  • 해당 키 아래의 특정 타입을 반환할 수 있음을 나타낸다.
  • 인터페이스의 객체는 일반적으로 문자열 키와 함께 사용된다
  • {[key: string]: ...}과 같이 사용된다

 

interface Seed {
  [key:string]: string
}

const seeds: Seed = {}

seeds.rose = '장미' // OK
seeds.sunflower = 0 // ERROR!!!!



인덱스 시그니처는 객체에 값을 할당할 때 편리하지만 타입 안정성을 완벽하게 보장하지는 않는다

 

interface DatesByName {
  [i:string]: Date;
}

const publishDates: DatesByName ={
  Frankenstein: new Date('1 January 1828')
}

publishDates.Frankenstein // OK

publishDates.Beloved;
// 🤔 Date 타입이겠군
// 🏃‍♂️ undefined

console.log(publishDates.Beloved.toString())
// 🤔 음~ Ok
// 🏃‍♂️ ERROR!!!!

 

키/값 쌍을 저장할 때 키를 미리 알 수 없으면 Map을 사용하는 편이 더 안전하다

속성과 인덱스 시그니처 혼합

interface Apples {
  factory: string ✨
  [key:string]:{
    color?: string
    name: string
    price: number
  }

  const newApple :Apples={
    factory: '사과공장',
    greenApple: {
      color: green,
      name: '초록사과',
      price: 500
    }
  }
  // OK

  const newApple2 :Apples={
    greenApple: {
      color: green,
      name: '초록사과',
      price: 500
    }
  }
  // ERROR!!! factory type이 missing

}

 

interface Apples {
  factory: '멋쟁이공장' ✨
  [key:string]:{
    color?: string
    name: string
    price: number
  }
}

=> Apples를 사용하는 모든 객체의 factory는 '멋쟁이공장'이 되어야 한다

 

속성과 인덱스 시그니처 혼합

 

객체의 키로 숫자만 허용할 때?

  • 명명된 속성은 타입을 포괄적인 용도의 string 인덱스 시그니처의 타입으로 할당해야 한다
interface moreNarrowNumbers {
  [i:number] : string
  [i:string] : string | undefined
}
// OK

interface moreNarrowNumbers {
  [i:string] : string
  [i:number] : string | undefined ✨ // ERROR!!!!
}

객체의 키가 숫자일 경우에도 obj.3 혹은 obj[3]으로 접근이 불가능하기 때문에, string 인덱스 시그니처의 타입으로 할당해야 한다!

 

6. 중첩 인터페이스

 

inteface fruit {
  factory:{
    name: string
    owner: string
  };
  setting: Setting;
}

interface Setting {
  name: string;
  color: string;
  price: number;
}

Setting이라는 interface를 interface fruit의 setting 하의 타입으로 중첩시켰다.
복잡한 interface 를 분할할 수 있고 재사용성이 용이해진다

 

 

 

 

📚 07.3 인터페이스 확장

 

타입스크립트는 인터페이스가 다른 인터페이스의 모든 멤버를 복사해 선언할 수 있는 확장된 인터페이스를 허용합니다

  • 확장할 인터페이스의 이름 뒤에 extends 키워드를 추가해서 표시
  • 파생 인터페이스를 준수하는 모든 객체는 기본 인터페이스의 모든 멤버도 가져야 한다
interface Bread {
  milk: string;
  butter: string;
  price: number
}

interface CoffeeBread extends Bread {
  coffee: string
}

const coffeeBurn : CoffeeBread ={
  milk: '상하목장',
  butter: '서울버터',
  price: 400,
  coffee: '믹스커피',
}

인터페이스 확장은 프로젝트의 한 엔티티 타입이 다른 엔티티의 모든 멤버를 포함하는 상위 집합을 나타내는 실용적인 방법

 

1. 재정의된 속성

 

파생 인터페이스는 다른 타입으로 속성을 다시 선언해 기본 인터페이스의 속성을 재정의 하거나 대체할 수 있습니다.

 

사용 이유

  • 유니언 타입의 더 구체적인 하위 집합으로 만들거나
  • 기본 인터페이스 타입에서 확장된 타입으로 만들기 위해
interface Cats {
  name: string
}

interface MyCats extends Cats {
  name: '나비' | '냥냥이' | '야옹이'
}

const myCat :MyCats = {
    name:'고양이' // ERROR!!!
}

const myCat :MyCats = {
    name:'냥냥이' // 😻 OK
}

interface NewCats extends Cats {
    name: number | undefined
}
// ERROR!!!!
// number | undefined는 string에 할당할 수 없음!

 

 

2. 다중 인터페이스 확장

 

interface Bread {
  flour: string;
  price: number;
}

interface MilkBread {
  milk: string;
}

interface ChocoBread {
  choco: string;
}

interface StrawberryBread {
  strawberry: number;
}

interface Cake {
  butter: string;
  cream: string;
}

interface MilkChocoStrawberryCake extends Bread, MilkBread, chocoBread, StrawberryBread, Cake

 

 

 

📚 07.4 인터페이스 병합

 

인터페이스의 특징 중 하나는 서로 병합하는 능력입니다

두 개의 인터페이스가 동일한 이름으로 동일 스코프에 선언된 경우,
선언된 모든 필드를 포함하는 더 큰 인터페이스가 코드에 추가됩니다

 

interface Merged {
  fromFirst: string
}

interface Merged {
  fromSecond: number
}

// 🤔
// interface Merged {
//   fromFirst: string
//   fromSecond: number
// }
// 이구나!

 

  • 일반적으로 인터페이스 병합을 자주 사용하지는 않는다
  • 외부 패키지 또는 Window같은 내장된 전역 인터페이스를 보강하는 데 유용하다
interface Window {
  myEnvironmentVariable: string;
}

window.myEnvironmentVariable //type :  string



  • 병합된 인터페이스는 타입이 다른 동일한 이름의 속성을 여러 번 선언할 수 없다
  • 이미 인터페이스에 선언되어 있다면 나중에 병합된 인터페이스에서도 동일한 타입을 사용해야 한다
interface MergedProperties {
  same: (input: boolean) => string;
  different: (input: string) => string;
}

interface MergedProperties {
  same: (input: boolean)=> string; // OK
  different: (input:number) => string;
  // ERROR!!!!!!!!!!!
}

 

  • 병합된 인터페이스는 타입이 다른 동일한 이름의 메서드을 정의할 수 있다(메서드에 대한 함수 오버로드 발생)
interface MergedMethods {
  different(input: string) : string;
}

interface MargedMethods {
  different(input: number): string ; // OK
}
반응형