아이템12 ~ 13까지 정리
- 아이템 12 : 타입 단언보다 타입 선언 사용하기
- 아이템 13 : 타입과 인터페이스의 차이점 알기
함수 표현식에 타입 적용하기
- 자바스크립트와 마찬가지로 타입스크립트 또한 문장(statement)과 함수 표현식(expression)을 다르게 인식한다.
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 => { /* ... */ };
함수 타입의 장점
- 불필요한 코드의 반복을 줄인다.
- 타입 적용 전
function add(a: number, b: number) { return a + b; } function sub(a: number, b: number) { return a - b; } function mul(a: number, b: number) { return a * b; } function div(a: number, b: number) { return a / b; }
- 타입 적용 후
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;
- 라이브러리를 만든다면 공통 함수 시그니처를 타입으로 제공할 수 있다.
- ex ) 리액트에서 MouseEvent 타입 대신 함수 전체에 적용할 수 있는 MouseEventHandler 타입을 제공하는 것처럼
다른 함수의 시그니처를 참조하고 싶다면
- 다른 함수의 시그니처를 참조하려면 typeof fn을 사용한다.
- 예로 들어 특정 리소스에 HTTP 요청을 보낸 후 response.json()을 이용해 응답 데이터를 추출하는 코드가 있을 때,
async function getQuote() {
const response = await fetch('/quote?by=Mark+Twain');
const quote = await response.json();
return quote;
}
// {
// "quote": "If you tell the truth, you don't have to remember anything.",
// "source": "notebook",
// "date": "1894"
// }
- 만약 /quote 가 존재하지 않는 API라면 404 에러가 뜬다. 또한 응답은 JSON 형식이 아닐 수 있다.
- 또한 fetch가 실패하면 거절된 프로미스를 응답하지 않기 때문에 상태 체크를 수행해줄 checkFetch 함수를 만들어보자.
// lib.dom.d.ts에 있는 fetch 함수를 참조한다.
declare function fetch(
input: RequestInfo, init?: RequestInit
): Promise<Response>;
typeof fetch
를 사용하여 타입스크립트가 input과 init 타입을 추론할 수 있도록 한다.- 시그니처 참조 또한 반환 타입을 보장하기 때문에 만약 throw 대신 return을 사용했다면 타입스크립트는 실수를 잡아낸다.
const checkedFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
// return new Error('Request failed: ' + response.status); 라고 쓴다면?
// Promise<Response | Error> 형식을 할당할 수 없다는 오류가 뜬다.
}
return response;
}
타입과 인터페이스의 차이점 알기
- 타입스크립트에서 named type을 정의하는 방법은 두 가지가 있다.
-
type
// 타입 선언 예시 type TState = { name: string; capital: string; }
-
interface
// 인터페이스 선언 예시 interface IState { name: string; capital: string; }
-
class (값으로도 쓰일 수 있는 런타임 개념이다. 아이템 8 참고)
-
인터페이스 선언과 타입의 비슷한 점
- 추가 속성과 함께 할당하면 오류가 발생한다.
- 인덱스 시그니처를 사용할 수 있다.
- 뭐였더라 ? 타입스크립트가 추가적인 속성을 예상할 수 있도록 하는 방법
-
함수 타입 지정이 가능하다.
// 함수 타입 지정 type TFn = (x: number) => string; interface IFn { (x: number): string; } const toStrT: TFn = x => '' + x; // OK const toStrI: IFn = x => '' + x; // OK // 자바스크립트에서 함수는 호출 가능한 객체이기 때문에 이런 문법도 가능하다. type TFnWithProperties = { (x: number): number; prop: string; } interface IFnWithProperties { (x: number): number; prop: string; }
-
타입과 인터페이스는 모두 제너릭을 사용할 수 있다.
type TPair<T> = { first: T; second: T; } interface IPair<T> { first: T; second: T; }
-
인터페이스는 타입을 확장할 수 있고, 타입은 인터페이스를 확장할 수 있다.
// 이렇게 사용하기 위해서 몇가지 주의사항이 있는데 추후 다루도록 한다. interface IStateWithPop extends TState { population: number; } type TStateWithPop = IState & { population: number; };
- IStateWithPop = TStateWithPop
- 예시처럼 인터페이스는 유니온 같은 복잡한 타입을 확장하지 못 한다.
- 만약 복잡한 타입을 확장하고 싶다면 타입과 &을 사용해야 한다.
- 클래스를 구현(implments)할 때는 타입과 인터페이스 둘 다 사용할 수 있다.
타입과 인터페이스의 다른 점
- 앞서 언급한 것처럼 유니온 타입은 있지만 유니온 인터페이스라는 개념은 없다.
- 인터페이스는 타입을 확장할 수 있지만, 유니온은 확장할 수 없다.
만약 유니온 타입을 확장하고 싶다면?
- Input | Output는 별도의 타입이며 이 둘의 하나의 변수명으로 매핑하는 VariableMap 인터페이스를 만들 수 있다.
type Input = { /* ... */ };
type Output = { /* ... */ };
interface VariableMap {
[name: string]: Input | Output;
}
- 또는 유니온 타입에 name 속성을 붙인 타입을 만들 수도 있다.
type NamedVariable = (Input | Output) & { name: string };
→ 이 타입은 인터페이스로 표현할 수 없다.
- type 키워드는 일반적으로 interface보다 쓰임새가 많다.
- 유니온이 될 수도 있고 매핑된 타입 또는 조건부 타입같은 고급 기능에 활용이 되기도 한다. (아직 어떻게 쓰는지 모르겠지만..)
- 튜플, 배열 타입도 type을 이용하는 것이 좋다.
- interface도 구현할 수 있지만 튜플에서 사용할 수 있는 concat 같은 메서드를 사용할 수 없게 된다.
interface Tuple { 0: number; 1: number; length: 2; } const t: Tuple = [10, 20]; // OK
type만 쓰면 되지 interface는 왜 쓰는 건가요?
- 인터페이스에는 타입에 없는 몇 가지 기능이 있다
- 인터페이스는
보강(augment)
이 가능하다.
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
}; // OK
- 이렇게 속성을 확장하는 방법을
선언 병합(declaration merging)
이라고 한다.- 주로 타입 선언 파일에서 사용한다.
따라서 타입 선언 파일을 작성할 때는 선언 병합을 지원하기 위해 반드시 인터페이스를 사용한다.
- 타입은 기존 타입에 추가적인 보강이 없는 경우에만 사용한다.
타입과 인터페이스 어떤 상황에서 어떻게 사용하면 좋을까?
- 어떤 API에 대한 타입 선언을 작성해야 한다면? → 인터페이스
- API가 변경될 때 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있어 유용하다.
- 프로젝트 내부적으로 사용되는 타입 → 타입
- 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계이다.
출처
- 이펙티브 타입스크립트