[TypeScript] 타입스크립트의 타입 시스템 (3)

김서현·2023년 4월 2일
0

TypeScript 스터디

목록 보기
4/4
post-thumbnail

📌 함수 표현식에 타입 적용하기

함수 '문장'과 함수 '표현식'

자바스크립트에서는 함수 '문장'과 함수 '표현식'을 다르게 인식한다.

  • 함수 문장
function rollDice1(sides: number): number {
  /*...*/
}
  • 함수 표현식
const rollDice2 = function (sides: number): number {
  /*...*/
};
const rollDice3 = (sides: number): number => {
  /* ... */
};

▶ 타입스크립트에서는 함수 표현식을 사용하는 것이 좋다!
함수의 매개변수부터 반환값까지 전체를 함수타입으로 선언하여 함수 표현식에 재사용할 수 있기 때문

type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => {/*...*/};
type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a+b;
const sub: BinaryFn = (a, b) => a-b;
const mul: BinaryFn = (a, b) => a*b;
const div: BinaryFn = (a, b) => a/b;

공통 함수 시그니처

함수 시그니처란?

  • 함수 타입 문법을 의미한다.
(a: number, b: number) => number;
(a: number, b: number): number;
  • 라이브러리는 공통 함수 시그니처를 타입으로 제공하기도 한다.
    ex) 리액트는 매개변수에 명시하는 MouseEvent 타입 대신에 함수 전체에 적용할 수 있는 MouseEventHandler 타입을 제공한다.

  • 시그니처가 일치하는 다른 함수가 있을 때, 함수 표현식에 타입 적용하기
    ex) 웹브라우저 fetch에 관련된 예시를 보자
    fetch : 특정 리소스에 HTTP 요청을 보내고, response.json() 또는 response.text()를 사용해 응답의 데이터를 추출

async function getQuote() {
  const response = await fetch(`/quote?by=Mark+Twain`);
  const quote = await response.json();
  return quote;
}

❓ 여기에 버그가 있다?

  • /quote가 존재하지 않는 API라면, '404 Not Found'가 포함된 내용을 응답함. 그 때 응답이 JSON 형식이 아니라면 오류 메시지를 담아 거절된 프로미스를 반환할 것. 이 오류 메시지에 의해 실제 오류인 404가 감추어진다.
  • fetch가 실패하면 거절된 프로미스를 응답하지 않는다는 것을 간과하기 쉽다!
    => 상태 체크를 해줄 checkedFetch 함수를 작성해 본다면
// 매개변수에 직접 타입 선언
async function checkedFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);
  if (!response.ok) {
    //비동기 함수 내에서 거절된 프로미스로 변환
    throw new Error("Request failed: " + response.status);
  }
  return response;
}

🔽이 코드를 지향하자🔽

// typeof fetch를 사용하여 함수 전체에 타입 선언
const checkedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    throw new Error("Request failed: " + response.status);
  }
  return response;
};

fetch의 타입 선언 (lib.dom.d.ts)

declare function fetch(
	input: RequestInfo, init?: RequestInit
 ): Promise<Response>;

▶ 왜 이 코드를 지양해야 하는가?
타입이 선언되어 있는 fetch의 타입을 이용함으로써

  • 타입스크립트가 input(RequestInfo), init(RequestInit)의 타입을 추론할 수 있게 해줄 뿐더러,
  • 반환 타입(Proise<Response>)까지 보장한다.

✅ 결론 : 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성하거나, 동일한 타입 시그니처를 가지는 여러 개의 함수를 작성할 때 매개변수의 타입과 반환타입을 반복해서 작성하지 말고 함수 전체의 타입 선언을 적용하자


📌 타입과 인터페이스의 차이점 알기

타입과 인터페이스

타입스크립트에서 명명된 타입을 정의하는 방법 두가지

  • 타입
type TState = {
  name: string;
  capital: string;
}
  • 인터페이스
interface IState {
  name: string;
  capital: string;
}

( 인터페이스 대신 클래스를 사용할 수도 있지만, 클래스는 값으로도 쓰일 수 있는 자바스크립트 런타임의 개념이다.)

타입 네이밍에 대여
❗ 교재에서는 인터페이스를 I, 타입을 T로 시작해 구분이 쉽게 표현하였으나, 실제 코드에서는 이렇게 해서는 안된다!
인터페이스 접두사를 붙이는 것은 C#에서 비롯된 관례로, 현재는 지양해야 할 스타일로 여겨진다.

  • 인터페이스 선언과 타입 선언의 비슷한 점
  1. 추가 속성과 함께 할당할 시 오류가 발생한다
const wyoming: TState = { // IState로 해도 마찬가지
    name: 'Wyoming',
    capital: 'Cheyenne',
    population: 500_500 // error: 'TState' 형식에 'population'이가 없습니다.
}
  1. 인덱스 시그니처를 사용할 수 있다.
type TDict = { [key: string]: string };
interface IDict {
  [key: string]: string;
}
  1. 함수 타입을 정의할 수 있다.
type TFn = (x: number) => string;
interface IFn {
    (x: number): string;
}

const toStrT: TFn = x => '' + x;
const toStrI: IFn = x => '' + x;
  1. 모두 제너릭이 가능하다
type TPair<T> = {
  first: T;
  second: T;
}
interface IPair<T> {
  first : T;
  second: T;
}
  1. 인터페이스와 타입은 서로를 확장할 수 있다.
interface IStateWithPop extends TState {
  population: number;
}
type TStateWithPop = IState & { population: number; };

❗ 그런데 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다!
복잡한 타입을 확장하고 싶다면 타입과 &를 사용해야 한다.

  1. 클래스를 구현할 때는, 타입과 인터페이스 둘 다 사용 가능하다.
class StateT implements TState {
  name: string = '';
  capital: string = '';
}
class StateI implements IState {
  name: string = '';
  capital: string = '';
}
  • 인터페이스 선언과 타입 선언의 다른 점
  1. 유니온 타입은 있지만, 유니온 인터페이스는 없다.
type AorB = 'a' | 'b';

❓ 그럼 인터페이스가 유니온 타입 확장이 필요하다면?

type Input = { /* */ };
type Output = { /* */ };
interface VaiableMap {
  [name: string] : Input | Output;
}

▶ 별도의 두 타입을 하나의 변수명으로 매핑하여 만들기

  1. type 키워드는 유니온이 될 수도 있고, 매핑된 타입 또는 조건부 타입 같은 고급 기능에 활용된다.
    🔽 아래 타입은 인터페이스로 표현할 수 없다.
type NamedVariable = (Input | Output) & {name: string};

또, 튜플과 배열 타입도 type 키워드를 이용해 더 간결하게 표현할 수 있다.

type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];

🔽 인터페이스로 비슷하게 구현할 수는 있다.

interface Tuple {
  0: number;
  1: number;
  length: 2;
}
const t: Tuple = [10, 20]

❗ 그러나 concat 같은 메서드들을 사용할 수는 없다.
✅ 따라서 튜플은 type 키워드로 구현하는 것이 낫다.

  1. 인터페이스만 가진 기능 - 보강(augment)
    선언 병합 : 속성을 아래처럼 확장하는 것.
interface IState {
  name: string;
  capital: string;
}
interface IState {
	population: number;
}
const wyoming: IState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000 //wjdtkd
};

✅ 결론

  • 복잡한 타입을 사용할 땐 타입 별칭을 사용하자.
  • 타입, 인터페이스 두 가지 방법으로 모두 표현 가능할 때는 인터페이스와 타입 중 일관된 것을 선택하자.
  • API에 대한 타입 선언을 작성할 때는 인터페이스를 사용하는 것이 좋다(인터페이스를 통해 새로운 필드를 병합할 수 있어 유용하기 때문)

타입 별칭이란?
특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미

// string 타입을 사용할 때
const name: string = 'capt';
// 타입 별칭을 사용할 때
type MyName = string;
const name: MyName = 'capt';

1개의 댓글

comment-user-thumbnail
2023년 4월 9일

함수 시그니처 부분 대박,, 생각지 못했던 부분인데 저런 버그가 생길 수도 있었네요..!! 배워갑니당 ㅎㅎ
그리고 type이랑 interface의 차이점을 항상 궁금해하기만 하고 찾아보지는 않았었는데 자세하게 정리해주셔서 확실하게 알게 됐어요 :) 감사합니다!

답글 달기