이 포스트는 TypeScript for JavaScript Programmers를 학습한 내용입니다.
TypeScript(TS)는 JavaScript(JS)의 모든 기능을 제공한다. 즉, 모든 JS 코드는 TS 코드이다. 여기에 TS는 타입 시스템을 추가로 제공한다.
JS에서는 string
, number
, object
등 다양한 값의 타입이 존재하지만, 변수에 할당된 값의 타입이 유지되는지 체크하지 않는다. 즉, 같은 변수에 다른 타입의 값을 재할당 할 수 있다.
let str = "cheese";
str = true;
반면, TS에서는 타입 추론을 통해 변수에 할당된 값의 타입으로 타입을 결정한다. 이후 값을 재할당하면 재할당된 값의 타입이 이전에 결정된 타입과 같은지 체크한다.
let str = "cheese";
str = true;
// Type 'boolean' is not assignable to type 'string'.
TS의 가장 큰 장점은 의도하지 않은 코드 실행을 런타임 이전에 방지하여 버그 발생의 가능성을 줄일 수 있다는 것이다.
1. 타입 추론 (Types by Inference)
TS를 사용하여 변수를 선언 후 값을 할당하면, 할당된 값의 타입을 타입으로 사용한다. 타입 추론을 통해 타입이 결정되므로 명시적으로 타입을 작성하지 않아도된다.
let helloWorld = "Hello World";
// let helloWorld: string = 'Hello World';
2. 타입 정의 (Defining Types)
JS 프로그래밍은 다양한 디자인 패턴을 통해 이루어진다. 이때 동적 프로그래밍을 사용하는 패턴은 타입 추론을 하기 어렵게 만든다. 이러한 경우 TS의 interface
, type
기능을 사용할 수 있다.
예를 들어 문자열 타입의 name
, 숫자 타입의 id
프로퍼티를 가진 객체는 아래와 같이 생성할 수 있다.
const user = {
name: "Jin",
id: 0,
};
interface
를 선언하여 위 객체의 구조를 명시적으로 작성할 수 있다.
interface User {
name: string;
id: number;
}
위에서 선언한 User
인터페이스를 따르는 객체를 생성하기 위해서는 : TypeName
문법을 사용한다.
const user: User = {
name: "Jin",
id: 0,
};
: TypeName
에 작성한 타입과 다른 타입의 값을 할당하면 에러가 발생한다.
interface User {
name: string;
id: number;
}
const user: User = {
username: "Jin",
id: 0,
};
// Type '{ username: string; id: number; }' is not assignable to type 'User'.
// Object literal may only specify known properties, and 'username' does not exist in type 'User'.
클래스로 생성한 인스턴스와 인터페이스를 함께 사용할 수도 있다.
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("Jin", 0);
인터페이스를 사용하여 함수의 매개변수와 리턴 값의 타입도 결정할 수 있다.
function getAdminUser(): User {
// ...
}
function deleteUser(user: User) {
// ...
}
JS에서 기본적으로 제공하는 값의 타입(boolean
, bigint
, null
, number
, string
, symbol
, object
, undefined
)을 인터페이스에서 그대로 사용할 수 있다. 여기에 TS는 any
, unknown
, never
, void
타입을 추가로 제공한다.
3. 타입 구성 (Composing Types)
Union, Generic을 사용하여 여러 타입을 하나의 타입으로 합칠 수 있다.
Unions
Union을 사용하여 타입을 선언할 수 있다. 예를 들어 true
혹은 false
타입의 값을 가지는 MyBool
타입을 아래와 같이 생성할 수 있다.
type MyBool = true | false;
// type MyBool = boolean
위 예제의
MyBool
타입은 구조적 타입 시스템(structural type system)에 의해boolean
타입으로 분류된다.
Union 타입은 유효한 리터럴 목록을 구조화할 때 사용할 수 있다.
type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type OddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
Union을 사용하면 리터럴 뿐만 아니라 여러 타입을 하나로 묶을 수 있다. 예를 들어, 매개변수의 타입이 array
혹은 string
인 함수를 아래와 같이 작성할 수 있다.
function getLength(obj: string | string[]) {
return obj.length;
}
변수의 타입을 확인하기 위해서는 typeof
연산자를 사용한다.
Type | 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];
} else {
return obj;
}
}
Generics
제네릭은 타입을 변수 형태로 전달하여 사용하는 방법이다. 예를 들어 일반 배열은 모든 타입의 값을 포함할 수 있다. 하지만 제네릭 배열은 전달된 타입 변수와 자신의 요소 타입이 일치해야하며, 이를 통해 자신의 요소가 무슨 타입인지 표현할 수 있다.
type StringArray = Array<string>; // ["foo", "bar", "baz"]
type NumberArray = Array<number>; // [1, 2, 3, 4]
type ObjectWithNameArray= Array<{ name: string }>; // [{ name: "Jin" }, { name: "Joon" }]
제네릭을 사용하는 커스텀 타입을 직접 선언할 수 있다.
interface BackPack<Type> {
add: (stuff: Type) => void;
get: () => Type;
}
declare const backpack: Backpack<string>;
// Backpack의 Type 자리에 string을 전달하였으므로 아래 stuff는 문자열 타입이다.
const stuff = backpack.get();
// Backpack의 Type 자리에 string을 전달하였으므로 숫자 타입은 add 할 수 없다.
backpack.add(23);
// Argument of type 'number' is not assignable to parameter of type 'string'.
4. 구조적 타입 시스템 (Structural Type System)
TS의 핵심 개념 중 하나는 타입 체크가 값의 구조(shape)에 의해 이루어진다는 것이다. 이러한 개념을 "duck typing" 혹은 "structural typing"이라 한다.
만약 두 객체가 같은 구조를 가지고 있다면, 이 둘은 구조적 타입 시스템에 의해 같은 타입으로 인식된다.
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
const point = { x: 12, y: 26 };
logPoint(point); // 12, 26
위 예제의 point
변수는 Point
타입으로 선언되지 않았다. 하지만 TS는 타입 체크 시 point
의 구조와 Point
구조를 비교하기 때문에 위 코드는 문제 없이 컴파일된다.
구조가 일치하는지 판단하는 기준은 프로퍼티 수가 더 적은 객체의 구조를 프로퍼티 수가 더 많은 객체가 충족하는지이다.
const point2 = { x: 15, y: 13: z: 89 };
logPoint(point2); // 15, 13
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // 33, 3
const color = { hex: "#187ABF" };
logPoint(color);
// Argument of type '{ hex: string; }' is not assignable to parameter of type 'Point'.
// Type '{ hex: string; }' is missing the following properties from type 'Point': x, y
이러한 규칙은 class를 사용할 때에도 동일하게 적용된다.
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); // 13, 56
Top comments (0)