JavaScript 프로그래머를 위한 TypeScript
JavaScript 프로그래머를 위한 TypeScript (TypeScript for JavaScript Programmers)
TypeScript는 JavaScript와 특이한 관계를 맺고 있어요. TypeScript는 JavaScript의 모든 기능을 제공하면서, 그 위에 한 층을 더 얹어요. 바로 TypeScript의 타입 시스템이죠. 예를 들어 JavaScript는 string과 number 같은 언어 기본 타입을 제공하지만, 여러분이 그 타입을 일관되게 할당했는지는 검사하지 않아요. TypeScript는 그걸 검사해요. 덕분에 기존에 작동하던 JavaScript 코드는 그대로 TypeScript 코드이기도 해요. TypeScript의 가장 큰 장점은 코드에서 예상 밖의 동작을 드러내 줘서 버그가 생길 가능성을 낮춰 준다는 점이에요.
출처: TypeScript 핸드북
이 튜토리얼은 TypeScript의 간단한 개요를, 특히 타입 시스템을 중심으로 다룰게요.
추론에 의한 타입 (Types by Inference)
TypeScript는 JavaScript 언어를 알고 있으며, 여러 경우에 여러분을 위해 타입을 생성해 줘요. 예를 들어 변수를 만들고 특정 값에 할당하면, TypeScript는 그 값을 타입으로 사용해요.
let helloWorld = "Hello World";
// ^?
TypeScript는 JavaScript가 어떻게 동작하는지 이해함으로써, JavaScript 코드를 받아들이면서도 타입이 있는 타입 시스템을 만들 수 있어요. 이는 코드에서 타입을 명시하기 위해 별도의 문자를 추가할 필요가 없는 타입 시스템을 제공한다는 뜻이에요. 그래서 위 예시에서 helloWorld가 string이라는 걸 TypeScript가 아는 거예요.
어쩌면 Visual Studio Code에서 JavaScript를 작성하면서 자동 완성 기능을 써 본 적이 있을 거예요. Visual Studio Code는 내부적으로 TypeScript를 사용해서 JavaScript를 더 쉽게 다룰 수 있게 해줘요.
타입 정의하기 (Defining Types)
JavaScript에서는 아주 다양한 디자인 패턴을 사용할 수 있어요. 하지만 어떤 디자인 패턴은 타입이 자동으로 추론되기 어렵게 만드는데, 예를 들어 동적 프로그래밍을 사용하는 패턴이 그래요. 이런 경우를 다루기 위해 TypeScript는 JavaScript 언어의 확장을 지원하고, 그 확장이 여러분이 TypeScript에 "타입이 이래야 한다"고 알려줄 자리를 제공해요.
예를 들어 name: string과 id: number를 포함하는 추론된 타입을 가진 객체를 만들려면 다음과 같이 쓸 수 있어요:
const user = {
name: "Hayes",
id: 0,
};
이 객체의 모양을 interface 선언으로 명시적으로 서술할 수도 있어요:
interface User {
name: string;
id: number;
}
그런 다음 변수 선언 뒤에 : TypeName 같은 문법을 사용해서 어떤 JavaScript 객체가 새 interface의 모양을 따른다고 선언할 수 있어요:
interface User {
name: string;
id: number;
}
// ---cut---
const user: User = {
name: "Hayes",
id: 0,
};
제공한 interface와 맞지 않는 객체를 만들면 TypeScript가 경고해 줘요:
// @errors: 2322
interface User {
name: string;
id: number;
}
const user: User = {
username: "Hayes",
id: 0,
};
JavaScript가 클래스와 객체지향 프로그래밍을 지원하므로 TypeScript도 지원해요. interface 선언을 클래스와 함께 사용할 수 있어요:
interface User {
name: string;
id: number;
}
class UserAccount {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
const user: User = new UserAccount("Murphy", 1);
interface를 사용해 함수의 매개변수와 반환 값을 주석(annotate)으로 달 수 있어요:
// @noErrors
interface User {
name: string;
id: number;
}
// ---cut---
function deleteUser(user: User) {
// ...
}
function getAdminUser(): User {
//...
}
JavaScript에는 이미 작은 기본 타입 집합이 있어요: boolean, bigint, null, number, string, symbol, undefined. 이것들은 interface 안에서 사용할 수 있어요. TypeScript는 이 목록에 몇 가지를 더 추가하는데, any (아무거나 허용), unknown (이 타입을 쓰는 사람이 타입이 무엇인지 명시하도록 강제), never (이 타입이 발생할 수 없음), 그리고 void (undefined를 반환하거나 반환 값이 없는 함수)가 있어요.
타입을 만드는 문법은 두 가지가 있다는 걸 알게 될 거예요: Interfaces와 Types. interface를 우선 사용하세요. 특정 기능이 필요할 때 type을 사용하세요.
타입 합성하기 (Composing Types)
TypeScript에서는 단순한 타입을 결합해서 복잡한 타입을 만들 수 있어요. 이를 위한 두 가지 인기 있는 방법은 유니언(union)과 제네릭(generic)이에요.
유니언 (Unions)
유니언을 사용하면 타입이 여러 타입 중 하나일 수 있다고 선언할 수 있어요. 예를 들어 boolean 타입을 true 또는 false 중 하나로 서술할 수 있어요:
type MyBool = true | false;
참고: 위 MyBool 위에 마우스를 올리면 boolean으로 분류되는 걸 볼 수 있어요. 이건 구조적 타입 시스템의 성질이에요. 이에 대해선 아래에서 더 다룰게요.
유니언 타입의 인기 있는 사용 사례는 값이 될 수 있는 string 또는 number 리터럴의 집합을 서술하는 거예요:
type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
유니언은 서로 다른 타입을 다루는 방법도 제공해요. 예를 들어 array 또는 string을 받는 함수를 만들 수 있어요:
function getLength(obj: string | string[]) {
return obj.length;
}
변수의 타입을 알아내려면 typeof를 사용해요:
| 타입 | 조건부 (Predicate) |
|---|---|
| string | typeof s === "string" |
| number | typeof n === "number" |
| boolean | typeof b === "boolean" |
| undefined | typeof undefined === "undefined" |
| function | typeof f === "function" |
| array | Array.isArray(a) |
예를 들어, 문자열을 받았는지 배열을 받았는지에 따라 다른 값을 반환하는 함수를 만들 수 있어요:
function wrapInArray(obj: string | string[]) {
if (typeof obj === "string") {
return [obj];
// ^?
}
return obj;
}
제네릭 (Generics)
제네릭은 타입에 변수를 제공해요. 흔한 예가 배열이에요. 제네릭이 없는 배열은 아무거나 담을 수 있어요. 제네릭이 있는 배열은 그 배열이 담는 값들을 서술할 수 있어요.
type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;
제네릭을 사용하는 자신만의 타입을 선언할 수도 있어요:
// @errors: 2345
interface Backpack<Type> {
add: (obj: Type) => void;
get: () => Type;
}
// This line is a shortcut to tell TypeScript there is a
// constant called `backpack`, and to not worry about where it came from.
declare const backpack: Backpack<string>;
// object is a string, because we declared it above as the variable part of Backpack.
const object = backpack.get();
// Since the backpack variable is a string, you can't pass a number to the add function.
backpack.add(23);
구조적 타입 시스템 (Structural Type System)
TypeScript의 핵심 원칙 중 하나는 타입 검사가 값이 가지는 모양(shape) 에 집중한다는 점이에요. 이를 "덕 타이핑(duck typing)" 또는 "구조적 타이핑(structural typing)"이라고 부르기도 해요.
구조적 타입 시스템에서는 두 객체가 같은 모양을 가지면 같은 타입으로 간주돼요.
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// logs "12, 26"
const point = { x: 12, y: 26 };
logPoint(point);
point 변수는 절대 Point 타입으로 선언된 적이 없어요. 하지만 TypeScript는 타입 검사에서 point의 모양을 Point의 모양과 비교해요. 같은 모양이므로 코드가 통과해요.
모양 일치는 객체 필드의 일부만 일치하면 돼요.
// @errors: 2345
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// ---cut---
const point3 = { x: 12, y: 26, z: 89 };
logPoint(point3); // logs "12, 26"
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // logs "33, 3"
const color = { hex: "#187ABF" };
logPoint(color);
클래스와 객체가 모양을 따르는 방식에는 차이가 없어요:
// @errors: 2345
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// ---cut---
class VirtualPoint {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const newVPoint = new VirtualPoint(13, 56);
logPoint(newVPoint); // logs "13, 56"
객체나 클래스가 필요한 프로퍼티를 모두 갖추고 있다면, 구현 세부 사항이 어떻든 TypeScript는 그들이 일치한다고 판단해요.
다음 단계 (Next Steps)
지금까지 일상 TypeScript에서 쓰이는 문법과 도구의 간략한 개요를 살펴봤어요. 여기서부터 다음과 같은 것들을 할 수 있어요:
- 전체 핸드북을 처음부터 끝까지 읽기
- Playground 예시 탐색하기