December 11, 2021 • ☕️☕️ 10 min read
타입스크립트의 타입 시스템
d.ts에서 타입 정의 확인 가능런타임에 모든 변수는 자바스크립트 세상의 값으로부터 정해지는 각자의 고유한 값을 가진다
그러나 코드가 실행되기 전, 즉 타입스크립트가 오류를 체크하는 순간에는 타입을 가지고 있으며, 이는 할당 가능한 값들의 집합이다.
집합의 종류
never - 아무것도 포함하지 않는 공집합. 아무 값도 할당 불가
const x: never = 12; // 🚨 '12' 형식은 'never' 형식에 할당할 수 없습니다.리터럴(유닛) 타입 - 한 가지 값만 포함하는 타입
type A = "A";유니온 타입 - 두 개 혹은 세 개, 값 집합들의 합집합
type AB = "A" | "B";‘할당 가능’하다는 뜻 - ‘부분 집합’
const a: AB = "A"; // 'A'는 집합 {'A', 'B'}의 원소실제 다루게 되는 타입은 대부분 범위가 무한대
type Int = 1 | 2 | 3 | 4 | 5; // | ...원소를 서술하는 방법
interface Identified {
id: string;
}타입 = 값의 집합
& 연산자는 두 타입의 인터섹션(교집합)을 계산
| 연산자는 두 인터페이스의 유니온, 교집합이 없는 두 개 이상의 타입에서 사용 시 주의
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
type K = keyof (Person | Lifespan); // 타입이 neverextends - ~에 할당 가능한, ~의 부분집합
서브타입 - 어떤 집합이 다른 집합의 부분집합
interface Vector1D {
x: number;
}
interface Vector2D extends Vector1D {
y: number;
}
interface Vector3D extends Vector2D {
z: number;
}Vector3D는 Vector2D의 서브타입, Vector2D는 Vector1D의 서브타입제네릭에서의 extends
function getKey<K extends string>(val: any, key: K) {
// ...
}집합의 관점에서 string을 상속
string 리터럴 타입, string 리터럴 타입의 유니온, string 자신을 포함타입이 집합이라는 관점에서 배열과 튜플의 관계 살펴보기
const list = [1, 2]; // 타입은 number[]
const tuple: [number, number] = list;
// 🚨 'number[]' 타입은 '[number, number]' 타입의 0, 1 속성에 없습니다트리플
const triple: [number, number, number] = [1, 2, 3];
const doulbe: [number, number] = triple;
// 🚨 숫자의 length값이 맞지 않기 때문에 할당문에 오류 발생타입이 값의 집합이라는 뜻은, 동일한 값의 집합을 가지는 두 타입은 같다는 의미
타입스크립트의 심벌(symbol)은 타입 공간이나 값 공간 중의 한 곳에 존재
interface Cylinder {
radius: number;
height: number;
}
const Cylinder = (radius: number, height: number) => ({ radius, height });interface Cylinder에서 Cylinder는 타입, const Cylinder와 무관
일반적으로 type이나 interface 다음에 나오는 심벌은 타입, const나 let 선언에 쓰이는 값은 값
class와 enum 은 상황에 따라 타입과 값 두 가지 모두 가능
// 타입으로 쓰인 Cylinder 클래스
class Cylinder {
radius = 1;
height = 1;
}
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
shape; // 정상, 타입은 Cylinder
shape.radius; // 정상, 타입은 number
}
}typeof - 타입에서 쓰일 때와 값에서 쓰일 때 다른 기능을 한다.
type T1 = typeof p; // 타입은 Person
type T2 = typeof email; // 타입은 (p: Person, subject: string, body: string) => Response
const v1 = typeof p; // 값은 'object'
const v2 = typeof email; // 값은 'function'typeof 은 값을 읽어서 타입스크립트 타입을 반환typeof 은 자바스크립트 런타임의 typeof 연산자를 반환 (심벌의 런타임 타입을 가리킴)클래스
type T = typeof Cylinder; // 타입이 typeof Cylinder
declare let fn: T;
const c = new fn(); // 타입이 CylinderInstanceType 제너릭을 사용해 생성자 타입과 인스턴스 타입 전환 가능
type C = InstanceType<typeof Cylinder>; // 타입이 Cylinder속성 접근자 []
obj['field']와 obj.field는 값이 동일하더라도 타입은 다를 수 있으므로, 타입의 속성을 얻을 때는 obj['field']를 사용한다👩🏫 타입스크립트에서 타입 공간과 값 공간을 혼동해서 잘못 작성하는 경우 주의
타입 단언은 오류를 발견하지 못한다
interface Person {
name: string;
}
const alice: Person = {}; // 🚨 'Person' 유형에 필요한 'name' 속성이 '{}' 유형에 없습니다.
const bob = {} as Person; // 오류 없음화살표 함수의 타입 선언
const people = ["alice", "bob", "jan"].map((name) => ({ name }));
// Person[]을 원했지만 결과는 { name: string; }[]...단언문 대신 화살표 함수의 반환 타입을 선언
const people = ["alice", "bob", "jan"].map((name): Person => ({ name })); // 타입은 Person[]그러나 함수 호출 체이닝이 연속되는 곳에서는 체이닝 시작에서부터 명명된 타입을 가져야 오류가 정확하게 표시된다
타입 단언이 꼭 필요한 경우
타입 체커가 추론한 타입보다 개발자가 판단하는 타입이 더 정확할 때
document.querySelector("#myButton").addEventListener("click", (e) => {
e.currentTarget; // 타입은 EventTarget
const button = e.currentTarget as HTMLButtonElement;
button; // 타입은 HTMLButtonElement
});! 문법을 사용해서 null이 아님을 단언하는 경우
const elNull = document.getElementById("foo"); // 타입은 HTMLElement | null
const el = document.getElementById("foo")!; // 타입은 HTMLElement타입 단언문으로 임의의 타입 간에 변환
A가 B의 부분집합(서브타입)인 경우 사용자바스크립트는 기본형과 객체 타입을 서로 자유롭게 변환 가능 (래퍼 객체)
string 기본형과 String 래퍼 객체가 항상 동일하게 동작하는 것은 아니다
String 객체는 오직 자기 자신하고만 동일하다
"hello" === new String("hello"); // false
new String("hello") === new String("hello"); // false타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링한다
function isGreeting(phrase: String) {
return ["hello", "good day"].includes(phrase);
// 🚨 'String' 형식의 인수는 'string' 형식의 매개변수에 할당될 수 없습니다.
}string은 String에 할당할 수 있지만 String은 string에 할당할 수 없다타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 ‘그 외의 속성은 없는지’ 확인한다.
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const obj = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: "present",
};
const r: Room = obj; // 정상obj 타입은 Room 타입의 부분 집합을 포함하므로, Room에 할당 가능하며 타입 체커도 통과한다타입스크립트는 런타임 오류 뿐 아니라 의도와 다르게 작성된 코드까지 찾는다
interface Options {
title: string;
darkMode?: boolean;
}
function createWindow(options: Options) {
if (options.darkMode) {
setDarkMode();
}
// ...
}
createWindow({
title: "Spider Solitaire",
darkmode: true, // 🚨 에러
});Options는 넓은 타입으로 해석된다
const o1: Options = document; // 정상
const o2: Options = new HTLMAnchorElement(); // 정상document와 HTLMAnchorElement의 인스턴스 모두 string 타입인 title 속성을 갖고 있기 때문에 할당문은 정상잉여 속성 체크는 객체 리터럴만 체크한다
const o: Options = { darkmode: true, title: "Ski Free" }; // 🚨 에러
const intermediate = { darkmode: true, title: "Ski Free" };
const o: Options = intermediate; // 정상
// 타입 단언문을 사용할 때도 적용되지 않는다
const o: Options = { darkmode: true, title: "Ski Free" } as Options; // 정상타입스크립트에서는 함수 선언문이 아닌 함수 표현식을 권장
함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있다
type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = (sides) => {
/* ... */
};반복되는 함수 시그니처를 하나의 함수로 통합하여 불필요한 코드의 반복을 줄일 수 있다
MouseEventHandler시그니처가 일치하는 다른 함수가 있을 때도 함수 표현식에 타입 적용 가능
ex) fetch 함수
const responseP = fetch("/quote?by=Mark+Twain"); // 타입이 Promise<Response>응답의 데이터를 추출
async function getQuote() {
const response = await fetch("/quote?by=Mark+Twain");
const quote = await response.json();
return quote;
}이때 /quote가 존재하지 않는 API거나 fetch가 실패하는 경우 버그가 발생한다
→ 상태 체크를 수행해 줄 checkedFetch 함수 작성
// lib.dom.d.ts
declare function fetch(
input: RequestInfo,
init?: RequestInit
): Promise<Response>;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;
};