자바스크립트에는 객체 이외에도 기본형 값들에 대한 일곱 가지 타입(string
, number
, boolean
, null
, undefined
, symbol
, bigint
)가 있다.
기본형들은 불변이고, 메서드를 가지지 않는단 점에서 객체와 구분되지만 기본형인 string
의 경우엔 메서드를 가지고 있는 것처럼 보인다.
'primitive'.charAt(3)
string
타입이 해당 메서드를 지니고 있는 것은 아니고, 자바스크립트에는 메서드를 가지는 String
객체 타입이 정의되어 있을 뿐이다. 자바스크립트는 기본형을 String
객체로 래핑해서 메서드를 호출하고, 마지막에 래핑한 객체를 버린다.
타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링하는데, 각 타입이 맞는지 유의해야 한다.
string
- String
number
- Number
boolean
- Boolean
symbol
- Symbol
bigint
- BigInt
특히 string
을 매개변수로 받는 메서드에 String
객체를 전달할 때 유의. 그리고 기본형 타입을 객체 래퍼에 할당하는 구문은 작성하지 말자. (ex. const s: String = "primitive"
)
단, new
없이 BigInt
와 Symbol
을 호출하는 경우엔 기본형을 생성하기 때문에 사용해도 좋음
typeof BigInt(1234) // "bigint"
typeof Symbol('sym') // "symbol"
타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 ‘그 외의 속성은 없는지’ 확인한다.
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const r: Room = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
// ~~~~~~~~~~~~~~~~~~~~ 개체 리터럴은 알려진 속성만 지정할 수 있으며
// 'Room' 형식에 'elephant'가 없습니다.
};
하지만 위는 구조적 타입 관점(item 4)에서 보면 오류가 발생하지 않아야 한다 → 포함 관계니까.
const obj = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
};
const r: Room = obj; // 정상
첫번째에서는 구조적 타입 시스템에서 발생할 수 있는 중요한 오류를 잡을 수 있도로 ‘잉여 속성 체크’ 과정이 수행됨. 하지만 ‘잉여 속성 체크’는 ‘할당 가능 검사’와는 별도의 과정임을 인지해야 한다.
interface Options {
title: string;
darkMode?: boolean;
}
const o1: Options = document; // document.title 이 존재하기 때문에 정상
const o2: Options = new HTMLAnchorElement; // HTMLAnchorElement.title 이 존재하기 때문에 정상
‘잉여 속성 체크는’ 객체 리터럴에 알 수 없는 속성을 허용하지 않음으로써 앞의 문제를 방지할 수 있다(=엄격한 리터럴 체크라고도 불림)
const intermediate = { darkmode: true, title: 'Ski Free' };
const o: Options = intermediate; // 정상
// 함수 표현식
const roll = function(sides: number): number { /* ... */ };
const roll = (sides: number): number => { /* ... */ };
// 함수 문장
function roll(sides: number): number { /* ... */ };
typeof fn
을 사용하자타입스크립트에서 타입을 정의하는 방법은 두 가지가 있다. 타입 or 인터페이스
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
대부분의 경우에는 둘 중 아무거나 사용해도 되지만, 차이점을 명확히 알고 적절하게 쓸 줄 알아야 한다.
여러 가지 공통점이 있지만 몇 가지를 꼽아보자면
// 인터페이스는 타입을 확장할 수 있고
interface IStateWithPop extends TState {
population: number;
}
// 타입은 인터페이스를 확장할 수 있다
type TStateWithPop = IState & { population: number; };
// 클래스를 구현할 때도 타입과 인터페이스 둘 다 사용할 수 있다.
class StateT implements TState {
name: string = '';
capital: string = '';
}
class StateI implements IState {
name: string = '';
capital: string = '';
}
단 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다 → 이러고 싶다면 type
과 &
를 사용해야 한다.
type
type AorB = 'A' | 'B';
// 이 타입은 인터페이스로는 표현할 수 없음
type NamedVaraible = (Input | Output) & { name: string };
type
키워드를 이용해 더 간결하게 표현할 수 있음.type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];
interface
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
}; // 정상
복잡한 타입이라면 고민할 것도 없이 type
을 사용하자.
타입과 인터페이스, 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성
과 보강
의 관점에서 고려해봐야 한다. -> 프로젝트 스타일에 따라 일관되게 쓰면 된다는 의미이다. 만약 아직 스타일이 확정되지 않았다면, 향후 보강 가능성이 있을 지 고려해서 정하자.
좋은 글 감사합니다.