일상적인 타입
일상적인 타입 (Everyday Types)
JavaScript 코드에서는 타입 몇 가지만 굉장히 자주 만나게 됩니다. 이 장은 문자열·숫자·배열부터 객체, 유니온, 인터페이스, any, 그리고 null과 undefined까지, 그 흔한 타입들을 TypeScript에서 어떻게 표현하는지 하나씩 살펴보는 시간이에요. 앞으로의 모든 장이 여기서 다룰 기본 타입을 조합해서 만들어지므로, 이 페이지가 사실상 TypeScript 문법의 기초 체력을 다지는 곳이라 할 수 있습니다.
출처: TypeScript 핸드북
이 장에서는 JavaScript 코드에서 찾을 수 있는 가장 흔한 값 타입 몇 가지를 다루고, 그 타입들을 TypeScript에서 설명하는 상응하는 방법을 설명할게요. 완전한 목록은 아니며, 이후의 장들에서 다른 타입에 이름을 붙이고 사용하는 더 많은 방법을 설명할 거예요.
타입은 타입 주석보다 훨씬 많은 위치 에 나타날 수 있어요. 타입 자체를 배우면서, 새 구조물을 만들기 위해 이 타입들을 참조할 수 있는 위치들도 함께 배우게 됩니다.
JavaScript 또는 TypeScript 코드를 작성할 때 만날 수 있는 가장 기본적이고 흔한 타입들을 검토하는 것으로 시작할게요. 이것들은 나중에 더 복잡한 타입의 핵심 구성 요소를 이룰 거예요.
원시 타입: string, number, boolean
JavaScript에는 세 가지 매우 흔히 사용되는 원시 타입이 있습니다: string, number, boolean입니다. 각각은 TypeScript에 상응하는 타입을 갖습니다. 예상할 수 있듯이, JavaScript의 typeof 연산자를 그 타입들의 값에 사용할 때 보이는 것과 같은 이름들입니다.
string은"Hello, world"같은 문자열 값을 나타냅니다.number는42같은 숫자를 위한 것입니다. JavaScript에는 정수에 대한 특별한 런타임 값이 없으므로int나float에 상응하는 것이 없고, 모든 것이 그저number입니다.boolean은true와false두 값에 대한 것입니다.
타입 이름
String,Number,Boolean(대문자로 시작하는)은 합법적이지만, 코드에 매우 드물게 나타날 특별한 내장 타입을 가리킵니다. 타입에는 항상string,number,boolean을 사용하세요.
배열 (Arrays)
[1, 2, 3] 같은 배열의 타입을 지정하려면 number[] 문법을 사용할 수 있습니다. 이 문법은 어떤 타입이든 동작해요 (예: string[]는 문자열의 배열입니다, 등등). 이것을 Array<number>로 쓰는 것도 볼 수 있을 텐데, 같은 뜻입니다. T<U> 문법에 대해서는 제네릭 을 다룰 때 더 배울 거예요.
[number]는 다른 것이라는 점에 주의하세요. 튜플 섹션을 참고하세요.
any
TypeScript에는 특정 값이 타입 검사 에러를 일으키지 않게 하고 싶을 때마다 사용할 수 있는 특별한 타입 any도 있습니다.
값이 any 타입일 때, 그 값의 어떤 프로퍼티든 접근할 수 있고(결국 그것도 any 타입이 됩니다), 함수처럼 호출할 수 있으며, 어떤 타입의 값에 (또는 그로부터) 할당할 수 있거나, 문법적으로 합법적인 거의 모든 것을 할 수 있습니다:
let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
any 타입은 특정 코드 줄이 괜찮다고 TypeScript를 설득하기 위해 긴 타입을 작성하고 싶지 않을 때 유용합니다.
noImplicitAny
타입을 지정하지 않고 TypeScript가 맥락에서 그것을 추론할 수 없을 때, 컴파일러는 보통 any로 기본 설정됩니다.
하지만 any는 타입 검사되지 않으므로 이것은 보통 피하고 싶을 거예요. 컴파일러 플래그 noImplicitAny를 사용해 암시적 any를 에러로 표시하세요.
변수에 대한 타입 주석 (Type Annotations on Variables)
const, var, let으로 변수를 선언할 때, 변수의 타입을 명시적으로 지정하기 위해 타입 주석을 선택적으로 추가할 수 있어요:
let myName: string = "Alice";
// ^^^^^^^^ Type annotation
TypeScript는
int x = 0;같은 "타입이 왼쪽에 오는" 스타일의 선언을 사용하지 않습니다. 타입 주석은 항상 타입이 지정되는 대상 뒤에 옵니다.
하지만 대부분의 경우 이것은 필요하지 않습니다. 가능한 곳마다 TypeScript는 코드의 타입을 자동으로 추론 하려고 합니다. 예를 들어 변수의 타입은 그 초기화자의 타입에 기반해 추론됩니다:
// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";
대체로 추론 규칙을 명시적으로 배울 필요는 없어요. 시작한다면, 생각보다 적은 타입 주석을 사용해 보세요 — TypeScript가 무슨 일이 일어나고 있는지 완전히 이해하는 데 필요한 주석이 얼마나 적은지 놀랄 수도 있습니다.
함수 (Functions)
함수는 JavaScript에서 데이터를 전달하는 주요 수단입니다. TypeScript는 함수의 입력과 출력 값 모두의 타입을 지정할 수 있게 해 줍니다.
매개변수 타입 주석 (Parameter Type Annotations)
함수를 선언할 때, 각 매개변수 뒤에 타입 주석을 추가해 함수가 받아들이는 매개변수의 타입을 선언할 수 있어요. 매개변수 타입 주석은 매개변수 이름 뒤에 옵니다:
// Parameter type annotation
function greet(name: string) {
// ^^^^^^^^
console.log("Hello, " + name.toUpperCase() + "!!");
}
매개변수가 타입 주석을 가질 때, 그 함수에 대한 인자는 검사됩니다:
// @errors: 2345
declare function greet(name: string): void;
// ---cut---
// Would be a runtime error if executed!
greet(42);
매개변수에 타입 주석이 없더라도 TypeScript는 올바른 개수의 인자를 전달했는지는 계속 검사합니다.
반환 타입 주석 (Return Type Annotations)
반환 타입 주석도 추가할 수 있어요. 반환 타입 주석은 매개변수 목록 뒤에 나타납니다:
function getFavoriteNumber(): number {
// ^^^^^^^^
return 26;
}
변수 타입 주석과 마찬가지로, 보통 반환 타입 주석은 필요 없습니다. TypeScript가 함수의 return 문에 기반해 반환 타입을 추론하거든요. 위 예시의 타입 주석은 아무것도 바꾸지 않습니다. 일부 코드베이스는 문서화 목적으로, 우발적인 변경을 막기 위해, 또는 개인적 선호로 반환 타입을 명시적으로 지정하기도 합니다.
Promise를 반환하는 함수
promise를 반환하는 함수의 반환 타입을 주석 처리하려면 Promise 타입을 사용해야 합니다:
async function getFavoriteNumber(): Promise<number> {
return 26;
}
익명 함수 (Anonymous Functions)
익명 함수는 함수 선언과는 조금 다릅니다. 함수가 TypeScript가 어떻게 호출될지 결정할 수 있는 위치에 나타날 때, 그 함수의 매개변수들은 자동으로 타입이 부여됩니다.
예시입니다:
// @errors: 2551
const names = ["Alice", "Bob", "Eve"];
// Contextual typing for function - parameter s inferred to have type string
names.forEach(function (s) {
console.log(s.toUpperCase());
});
// Contextual typing also applies to arrow functions
names.forEach((s) => {
console.log(s.toUpperCase());
});
매개변수 s에 타입 주석이 없었음에도, TypeScript는 forEach 함수의 타입과 배열의 추론된 타입을 사용해 s가 가질 타입을 결정했습니다.
이 과정은 상황적 타이핑(contextual typing) 이라고 불리는데, 이는 함수가 발생한 상황 이 그것이 가져야 할 타입을 알려 주기 때문입니다.
추론 규칙과 유사하게, 이것이 어떻게 일어나는지 명시적으로 배울 필요는 없습니다. 하지만 그것이 일어난다는 것을 이해하면 타입 주석이 필요하지 않은 때를 알아차리는 데 도움이 될 수 있어요. 나중에 값이 발생하는 상황이 그 타입에 어떻게 영향을 줄 수 있는지에 대한 더 많은 예시를 볼 거예요.
객체 타입 (Object Types)
원시 타입 외에, 여러분이 만날 가장 흔한 종류의 타입은 객체 타입 입니다. 이것은 프로퍼티를 가진 모든 JavaScript 값을 가리키는데, 거의 모든 값이 그렇죠! 객체 타입을 정의하려면, 그 프로퍼티들과 그 타입들을 나열하면 됩니다.
예를 들어, point 같은 객체를 받는 함수가 있습니다:
// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
// ^^^^^^^^^^^^^^^^^^^^^^^^
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
여기서 우리는 x와 y라는 두 프로퍼티를 가진 타입으로 매개변수를 주석 처리했고, 둘 다 number 타입입니다. 프로퍼티를 구분하기 위해 , 또는 ;를 사용할 수 있고, 마지막 구분자는 어느 쪽이든 선택 사항입니다.
각 프로퍼티의 타입 부분도 선택 사항입니다. 타입을 지정하지 않으면 any로 간주됩니다.
선택적 프로퍼티 (Optional Properties)
객체 타입은 또한 그 프로퍼티 중 일부 또는 전부가 선택적 임을 지정할 수 있습니다. 이렇게 하려면 프로퍼티 이름 뒤에 ?를 추가하면 됩니다:
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
JavaScript에서 존재하지 않는 프로퍼티에 접근하면 런타임 에러가 아니라 undefined 값을 얻게 됩니다. 이 때문에 선택적 프로퍼티에서 읽을 때는 사용하기 전에 undefined를 확인해야 합니다.
// @errors: 18048
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// A safe alternative using modern JavaScript syntax:
console.log(obj.last?.toUpperCase());
}
유니온 타입 (Union Types)
TypeScript의 타입 시스템은 다양한 연산자를 사용해 기존 타입으로부터 새 타입을 만들 수 있게 해 줍니다. 이제 우리는 몇 가지 타입을 작성하는 법을 알았으니, 그것들을 흥미로운 방식으로 조합 하기 시작할 때입니다.
유니온 타입 정의하기
먼저 보게 될 타입 조합 방법은 유니온 타입입니다. 유니온 타입은 두 개 이상의 다른 타입으로 형성된 타입으로, 그 타입들 중 어느 하나 일 수 있는 값을 나타냅니다. 우리는 이 각각의 타입을 유니온의 멤버 라고 부릅니다.
문자열이나 숫자에 대해 동작할 수 있는 함수를 작성해 봅시다:
// @errors: 2345
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
유니온 멤버의 구분자는 첫 번째 요소 앞에서도 허용되므로, 다음과 같이 쓸 수도 있습니다:
function printTextOrNumberOrBool( textOrNumberOrBool: | string | number | boolean ) { console.log(textOrNumberOrBool); }
유니온 타입 다루기
유니온 타입과 일치하는 값을 제공 하는 것은 쉽습니다 — 유니온의 멤버 중 하나와 일치하는 타입을 제공하기만 하면 되죠. 그런데 유니온 타입의 값이 있다면, 어떻게 다룰까요?
TypeScript는 그 연산이 유니온의 모든 멤버에 대해 유효한 경우에만 허용합니다. 예를 들어 string | number 유니온이 있다면, string에서만 사용 가능한 메서드를 사용할 수 없어요:
// @errors: 2339
function printId(id: number | string) {
console.log(id.toUpperCase());
}
해결책은 타입 주석 없는 JavaScript에서 하는 것과 똑같이, 코드로 유니온을 좁히는(narrow) 것입니다. 좁히기 는 TypeScript가 코드의 구조에 기반해 값에 대해 더 구체적인 타입을 추론할 수 있을 때 발생합니다.
예를 들어 TypeScript는 string 값만이 typeof 값 "string"을 가질 것임을 압니다:
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
또 다른 예시는 Array.isArray 같은 함수를 사용하는 것입니다:
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
else 분기에서 우리는 특별한 일을 할 필요가 없다는 점을 주목하세요 — x가 string[]이 아니라면, string이었음이 틀림없으니까요.
때로는 유니온의 모든 멤버가 공통된 무언가를 가진 경우가 있어요. 예를 들어 배열과 문자열 둘 다 slice 메서드를 갖습니다. 유니온의 모든 멤버가 공통 프로퍼티를 갖는다면, 좁히기 없이 그 프로퍼티를 사용할 수 있어요:
// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
타입의 유니온 이 그 타입들의 프로퍼티의 교집합 을 갖는 것처럼 보이는 것이 혼란스러울 수 있어요. 이것은 우연이 아닙니다 — 유니온 이라는 이름은 타입 이론에서 유래했습니다. 유니온
number | string은 각 타입의 값의 유니온을 취함으로써 구성됩니다. 각 집합에 대한 상응하는 사실이 있는 두 집합이 주어졌을 때, 집합들 자체의 유니온 에는 그 사실들의 교집합 만 적용된다는 점을 주목하세요. 예를 들어 모자를 쓴 키 큰 사람들로 가득한 방과, 모자를 쓴 스페인어 화자들로 가득한 다른 방이 있다면, 두 방을 합친 후 모든 사람에 대해 아는 유일한 것은 그들이 모자를 쓰고 있어야 한다는 것입니다.
타입 별칭 (Type Aliases)
우리는 지금까지 객체 타입과 유니온 타입을 타입 주석 안에 직접 작성하며 사용해 왔어요. 이것은 편리하지만, 같은 타입을 여러 번 사용하고 단일 이름으로 참조하고 싶은 것은 흔한 일입니다.
타입 별칭 은 정확히 그것입니다 — 어떤 타입에 대한 이름 이죠. 타입 별칭의 문법은 다음과 같습니다:
type Point = {
x: number;
y: number;
};
// Exactly the same as the earlier example
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
실제로 타입 별칭을 사용해 객체 타입뿐 아니라 어떤 타입이든 이름을 지을 수 있어요. 예를 들어 타입 별칭은 유니온 타입에 이름을 붙일 수 있습니다:
type ID = number | string;
별칭은 오직 별칭일 뿐이라는 점에 주의하세요 — 타입 별칭을 사용해 같은 타입의 다른/구별되는 "버전"을 만들 수는 없습니다. 별칭을 사용하면 별칭이 지정된 타입을 쓴 것과 정확히 같습니다. 다시 말해, 이 코드는 불법 처럼 보일 수 있지만 두 타입 모두 같은 타입의 별칭이기 때문에 TypeScript에 따르면 괜찮습니다:
declare function getInput(): string;
declare function sanitize(str: string): string;
// ---cut---
type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}
// Create a sanitized input
let userInput = sanitizeInput(getInput());
// Can still be re-assigned with a string though
userInput = "new input";
인터페이스 (Interfaces)
인터페이스 선언 은 객체 타입에 이름을 붙이는 또 다른 방법입니다:
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
위에서 타입 별칭을 사용했을 때와 정확히 마찬가지로, 이 예시는 익명 객체 타입을 사용한 것처럼 동작합니다. TypeScript는 우리가 printCoord에 전달한 값의 구조 에만 관심이 있습니다 — 그것이 예상된 프로퍼티를 갖고 있기만 하면 되죠. 타입의 구조와 능력에만 관심을 갖는 것이 우리가 TypeScript를 구조적 타입 시스템이라고 부르는 이유입니다.
타입 별칭과 인터페이스의 차이점
타입 별칭과 인터페이스는 매우 비슷하며, 많은 경우 자유롭게 선택할 수 있어요. interface의 거의 모든 기능은 type에서 사용 가능하지만, 핵심적인 구별은 타입은 새 프로퍼티를 추가하도록 다시 열 수 없는 반면 인터페이스는 항상 확장 가능하다는 것입니다.
Interface |
Type |
|---|---|
|
Extending an interface
|
Extending a type via intersections
|
|
Adding new fields to an existing interface
|
A type cannot be changed after being created
|
이 개념들에 대한 더 자세한 내용은 이후 장에서 배우게 될 거예요. 지금 당장 이해하지 못해도 걱정하지 마세요.
- TypeScript 4.2 버전 이전에는 타입 별칭 이름이 때로는 에러 메시지에 나타날 수 있는데, 때로는 상응하는 익명 타입 대신 (바람직할 수도 그렇지 않을 수도). 인터페이스는 에러 메시지에 항상 이름이 지정됩니다.
- 타입 별칭은 선언 병합에 참여할 수 없지만, 인터페이스는 가능합니다.
- 인터페이스는 객체의 모양을 선언하는 데만 사용할 수 있고, 원시 타입의 이름을 바꾸는 데는 사용할 수 없습니다.
- 인터페이스 이름은 이름으로 사용될 때 항상 원래 형태로 에러 메시지에 나타나지만, 오직 이름으로 사용될 때만입니다.
- 인터페이스와
extends를 함께 사용하는 것은 교차(intersection)를 사용하는 타입 별칭보다 컴파일러에 종종 더 성능이 좋을 수 있습니다.
대체로 개인적 선호에 따라 선택할 수 있고, TypeScript가 다른 종류의 선언이 필요한지 알려줄 거예요. 휴리스틱을 원한다면 type의 기능이 필요해질 때까지 interface를 사용하세요.
타입 단언 (Type Assertions)
때로는 TypeScript가 알 수 없는 값의 타입에 대한 정보를 당신이 갖고 있을 때가 있어요.
예를 들어 document.getElementById를 사용한다면, TypeScript는 이것이 어떤 종류의 HTMLElement를 반환할 것이라고만 압니다. 하지만 당신은 페이지에 특정 ID를 가진 HTMLCanvasElement가 항상 있을 것임을 알 수도 있어요.
이 상황에서는 타입 단언 을 사용해 더 구체적인 타입을 지정할 수 있습니다:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
타입 주석처럼, 타입 단언은 컴파일러에 의해 제거되며 코드의 런타임 동작에 영향을 주지 않습니다.
(.tsx 파일이 아니라면) 각괄호 문법도 사용할 수 있는데, 이는 동등합니다:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
알림: 타입 단언은 컴파일 타임에 제거되므로, 타입 단언과 관련된 런타임 검사는 없습니다. 타입 단언이 잘못되어도 예외나
null이 생성되지 않습니다.
TypeScript는 타입을 더 구체적인 버전 또는 덜 구체적인 버전으로 변환하는 타입 단언만 허용합니다. 이 규칙은 다음과 같은 "불가능한" 강제 변환을 방지합니다:
// @errors: 2352
const x = "hello" as number;
때로는 이 규칙이 너무 보수적이어서 유효할 수 있는 더 복잡한 강제 변환을 허용하지 않을 수 있어요. 이런 일이 발생하면, 먼저 any(또는 나중에 소개할 unknown)로, 그다음 원하는 타입으로 두 번 단언할 수 있습니다:
declare const expr: any;
type T = { a: 1; b: 2; c: 3 };
// ---cut---
const a = expr as any as T;
리터럴 타입 (Literal Types)
일반 타입인 string과 number 외에도, 타입 위치에서 특정한 문자열과 숫자를 참조할 수 있어요.
이것을 생각하는 한 가지 방법은 JavaScript가 변수를 선언하는 여러 방식과 함께 오는 방식을 고려하는 것입니다. var와 let 둘 다 변수 안에 담긴 것을 바꿀 수 있게 허용하고, const는 그렇지 않습니다. 이것은 TypeScript가 리터럴에 대한 타입을 만드는 방식에 반영됩니다.
let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString;
// ^?
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString;
// ^?
리터럴 타입 그 자체로는 그다지 가치가 없습니다:
// @errors: 2322
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
하나의 값만 가질 수 있는 변수를 갖는 것은 별로 쓸모가 없어요!
하지만 리터럴을 유니온으로 조합 하면 훨씬 유용한 개념을 표현할 수 있습니다 — 예를 들어 특정 알려진 값 집합만 받아들이는 함수 말이죠:
// @errors: 2345
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
숫자 리터럴 타입도 같은 방식으로 동작합니다:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
물론 이것들을 비-리터럴 타입과 조합할 수도 있습니다:
// @errors: 2345
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
리터럴 타입의 한 가지 더 종류가 있습니다: boolean 리터럴입니다. boolean 리터럴 타입은 두 개뿐이고, 예상할 수 있듯이 그것들은 true와 false 타입입니다. boolean 타입 자체는 실제로 유니온 true | false의 별칭일 뿐입니다.
리터럴 추론 (Literal Inference)
변수를 객체로 초기화할 때, TypeScript는 그 객체의 프로퍼티가 나중에 값을 바꿀 수 있다고 가정합니다. 예를 들어 다음과 같은 코드를 작성했다면:
declare const someCondition: boolean;
// ---cut---
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
TypeScript는 이전에 0이었던 필드에 1을 할당하는 것을 에러로 간주하지 않습니다. 이것을 다르게 말하면, obj.counter는 0이 아니라 number 타입이어야 한다는 것인데, 타입은 읽기 및 쓰기 동작을 결정하는 데 모두 사용되기 때문입니다.
같은 것이 문자열에도 적용됩니다:
// @errors: 2345
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
위 예시에서 req.method는 "GET"이 아니라 string으로 추론됩니다. req의 생성과 handleRequest의 호출 사이에 req.method에 "GUESS" 같은 새 문자열을 할당할 수 있는 코드가 평가될 수 있기 때문에, TypeScript는 이 코드를 에러가 있다고 간주합니다.
이를 해결하는 두 가지 방법이 있어요.
-
어느 위치에든 타입 단언을 추가해 추론을 바꿀 수 있습니다:
declare function handleRequest(url: string, method: "GET" | "POST"): void; // ---cut--- // Change 1: const req = { url: "https://example.com", method: "GET" as "GET" }; // Change 2 handleRequest(req.url, req.method as "GET");변경 1은 "
req.method가 항상 리터럴 타입"GET"을 갖도록 의도한다"는 뜻이며, 이후 그 필드에"GUESS"를 할당하는 가능성을 막습니다. 변경 2는 "다른 이유로req.method가"GET"값을 가진다는 것을 안다"는 뜻입니다. -
as const를 사용해 전체 객체를 타입 리터럴로 변환할 수 있습니다:declare function handleRequest(url: string, method: "GET" | "POST"): void; // ---cut--- const req = { url: "https://example.com", method: "GET" } as const; handleRequest(req.url, req.method);
as const 접미사는 const처럼 작동하되 타입 시스템을 위한 것이며, 모든 프로퍼티가 string이나 number 같은 더 일반적인 버전 대신 리터럴 타입을 할당받도록 보장합니다.
null과 undefined
JavaScript에는 부재하거나 초기화되지 않은 값을 알리는 데 사용되는 두 원시 값이 있습니다: null과 undefined입니다.
TypeScript에는 같은 이름의 상응하는 타입 이 두 개 있습니다. 이 타입들이 어떻게 동작하는지는 strictNullChecks 옵션을 켰는지에 달려 있습니다.
strictNullChecks 꺼짐
strictNullChecks가 꺼져 있으면, null 또는 undefined일 수 있는 값도 정상적으로 접근할 수 있고, null과 undefined 값은 어떤 타입의 프로퍼티에도 할당될 수 있습니다. 이것은 null 검사가 없는 언어(예: C#, Java)가 동작하는 방식과 비슷합니다. 이러한 값들에 대한 검사 부족은 주요 버그 원인이 되는 경향이 있습니다. 가능하다면 코드베이스에서 strictNullChecks를 켜는 것을 항상 권장합니다.
strictNullChecks 켜짐
strictNullChecks가 켜져 있으면, 값이 null 또는 undefined일 때 그 값의 메서드나 프로퍼티를 사용하기 전에 그 값들을 먼저 검사해야 합니다. 선택적 프로퍼티를 사용하기 전에 undefined를 검사하는 것처럼, null일 수 있는 값을 검사하기 위해 좁히기 를 사용할 수 있어요:
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
Non-null 단언 연산자 (Postfix !)
TypeScript에는 명시적인 검사 없이 타입에서 null과 undefined를 제거하는 특별한 문법도 있습니다. 어떤 표현식 뒤에 !를 쓰는 것은 그 값이 null 또는 undefined가 아니라는 효과적인 타입 단언입니다:
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
다른 타입 단언처럼, 이것은 코드의 런타임 동작을 바꾸지 않으므로, 값이 null 또는 undefined가 될 수 없다는 것을 알 때만 !를 사용하는 것이 중요합니다.
Enum
Enum은 TypeScript가 JavaScript에 추가한 기능으로, 가능한 명명된 상수 집합 중 하나일 수 있는 값을 설명할 수 있게 해 줍니다. 대부분의 TypeScript 기능과 달리 이것은 JavaScript에 대한 타입 수준의 추가가 아니라 언어와 런타임에 추가되는 것입니다. 이 때문에 이것은 존재한다는 걸 알아두어야 하지만, 확실하지 않다면 사용을 보류하길 권하는 기능이에요. Enum 참조 페이지에서 enum에 대해 더 읽을 수 있습니다.
덜 흔한 원시 타입들 (Less Common Primitives)
타입 시스템에 표현되는 JavaScript의 나머지 원시 타입들도 언급할 가치가 있어요. 여기서 깊이 다루지는 않을 거예요.
bigint
ES2020부터 아주 큰 정수에 사용되는 JavaScript 원시 타입 BigInt가 있습니다:
// @target: es2020
// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;
BigInt에 대해 더 배우고 싶다면 TypeScript 3.2 릴리스 노트를 참고하세요.
symbol
Symbol() 함수를 통해 전역적으로 고유한 참조를 만드는 데 사용되는 JavaScript 원시 타입이 있습니다:
// @errors: 2367
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// Can't ever happen
}
이것들에 대해 Symbols 참조 페이지에서 더 배울 수 있습니다.