js에서는 함수를 선언할때 함수 문장과 함수 표현식을 사용하는데
function dice(input) { } // 함수 문장
const dice = (input) => { } // 함수 표현식
ts에서는 함수 표현식을 사용하는 것이 좋다.
1. 함수의 매개변수 부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다.
type Add = (num: number) => number;
const add: Add = num => { /* ... */ }
2. 불필요한 타입 선언 코드의 반복을 줄인다.
함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다
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;
3. 공통 콜백 함수를 위한 타입 선언을 제공할 수 있다
만약 다른 함수의 시그니처를 참조하려면 typeof fn 을 사용하면 된다.
기존에 있는 함수와 동일한 시그니처 사용에도 함수 표현식이 좋은데
fetch 함수는 요청이 실패했을때 reject(실패) 된 promise 를 반환하지 않는다. 함수를 호출한 곳에서 사용하기 쉽도록 커스텀 함수를 작성할 때 함수 표현식으로 작성하면 기존 타입을 재사용할 수 있다.
const customFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
return response.ok ? response : throw new Error('Request failed')
}
ts에서 명명된 타입(named type)을 정의하는 type 과 interface 가 있다.
(type과 interface에 접두사로 책에서 T/I를 붙이는 것은 C#에서 영향을 받았는데 ts에서는 지양하는게 좋다고 한다.)
1. 타입 상태에는 차이가 없다.
type TPerson = {
name: string,
}
interface IPerson {
name: string;
}
// 두 타입 모두 상태가 동일하다.
// 두 타입이 지정된 변수에 알려진 객체 리터럴을 할당하려고 하면 동일한 방법으로 타입 체크를 진행한다.
2. 인덱스 시그니처 사용 가능
type TPerson = {
[key: string]: string,
}
interface IPerson {
[key: string]: string;
}
3. 함수 타입도 정의 가능
type TFn = (x: number) => string;
interface IFn {
(x: number): string;
}
4. 제네릭이 가능하다.
type TPair<T> = {
first: T;
second: T;
}
interface IPair<T> {
first: T;
second: T;
}
5. 인터페이스는 타입을 확장할 수 있으며, 타입은 인터페이스를 확장할 수 있다.
interface IStateWithPop extends TState {
population: number;
}
type TStateWithPop = IState & { population: number; };
// IStateWithPop 와 TStateWithPop 는 동일하다.
💦 단, 인터페이스는 유니온 타입 처럼 복잡한 타입을 확장하지 못한다. 복잡한 타입을 확장하고 싶다면 타입과 & 를 사용해야한다.
6. 클래스를 구현(implements)할 수 있다.
class StateT implements TState {
name: string = '';
capital: string = '';
}
class StateI implements IState {
name: string = '';
capital: string = '';
}
1. 인터페이스는 타입을 확장할 수 있지만, 유니온을 확장할 순 없다.
다음 처럼 복잡한 타입은 인터페이스로 표현할 수 없다.
type NamedVariable = (Input | Output) & { name: string };
즉, 타입은 유니온 (Input | Output) 을 & 로 확장할 수 있지만
인터페이스로 유니온을 확장할 수는 없다.
2. 인터페이스로 튜플 타입을 완벽하게 구현할 수 없다.
타입은 아래 처럼 튜플을 간결하게 표현할 수 있다.
type Pair = [number, number];
하지만 인터페이스로는 아래처럼 비슷하게 구현할 수 는 있지만, Array 고차함수를 사용할 수는 없다.
interface Pair {
0: number;
1: number;
length: 2;
}
(튜플(tuple)이란 자료 구조로, 배열처럼 여러 개의 데이터를 열거하여 담아두는 데에 사용한다.)
3. 인터페이스는 보강(argument)이 가능하다.
인터페이스는 아래 예제처럼 선언 병합(declaration merging) 이 가능하다.
아래의 코드를 보면 인터페이스가 중복으로 선언된 것 같지만 올바른 문법이고 아래처럼 속성을 확장하는 것을 선언병합 이라고 한다.
interface IPerson {
name: string;
}
interface IPerson {
age: number;
}
const hee: IPerson = {
name: 'hee',
age: 25
} // 정상
선언 병합은 주로 타입 선언 파일에서 사용한다. 선언 병합은 인터페이스에서만 가능하다.
ts는 여러 버전의 js 표준 라이브러리에서 여러 타입을 모아 병합한다. 병합을 통해 다른 인터페이스에 추가되어 하나의 타입에 버전별로 메서드들을 모을 수 있다.
- API처럼 사용자가 인터페이스를 통해 새로운 필드를 병합해야 하는 경우엔 인터페이스를 사용하고, 간단하고 일관된 타입을 사용한다면 타입을 사용하면 된다.
- 어지간하면 타입이 좋다.
보강의 가능성이 있을 경우 (버전) => interface 사용
API 타입 선언 등 => interface 사용 (새로운 필드 병합)
인터페이스와 타입은 서로 비슷하지만, 사용하는 상황에 따라서 선택해야 한다. 인터페이스는 객체의 구조를 설명하거나 다른 인터페이스와 결합할 때 사용하는 경우가 많은 반면, 타입은 값을 묘사하고, 유니온 타입과 인터섹션 타입과 같은 고급 타입을 사용해야 할 때 유용하다.