일상에서 쓰는 타입

일상에서 쓰는 타입 (Everyday Types)

JavaScript 코드를 짜다 보면 마주치는 값의 타입이 사실 몇 가지 안 돼요. 이번 장에서는 그런 가장 흔한 타입들을 하나씩 살펴보고, 각각을 TypeScript에서는 어떻게 표현하는지 배워볼게요. 여기 있는 목록이 전부는 아니고, 뒤에 나오는 장들에서 더 다양한 타입을 다룰 거예요.

주의 깊게 볼 점이 하나 있어요. 타입은 타입 주석(annotation) 말고도 훨씬 더 다양한 자리에 등장할 수 있거든요. 타입 자체를 배우면서, 동시에 "이 타입들을 어디에 써서 새로운 것을 만들어 낼 수 있는지"도 같이 보게 될 거예요.

출처: TypeScript 공식문서

본문

일단 JavaScript나 TypeScript 코드를 쓸 때 가장 기본적이고 자주 만나는 타입들부터 훑어볼게요. 이 친구들이 나중에 더 복잡한 타입을 만들 때 쓰는 기초 재료가 돼요.

원시 타입: string, number, boolean

JavaScript에는 아주 흔하게 쓰는 원시 타입(primitives)이 셋 있어요. string, number, boolean. 그리고 각각에 대응하는 타입이 TypeScript에도 그대로 있어요. 짐작하셨겠지만, 이 이름들은 JavaScript에서 해당 값에 typeof 연산자를 썼을 때 나오는 이름과 똑같아요.

  • string"Hello, world" 같은 문자열 값을 뜻해요.
  • number42 같은 숫자를 뜻해요. JavaScript에는 정수를 위한 별도의 런타임 값이 없어서, intfloat 같은 건 없고 전부 그냥 number예요.
  • booleantruefalse 두 값만 뜻해요.

대문자로 시작하는 String, Number, Boolean이라는 타입 이름도 문법상으로는 유효해요. 다만 이들은 코드에서 거의 등장하지 않는 특별한 내장 타입을 가리키거든요. 타입을 쓸 때는 항상 소문자인 string, number, boolean을 쓰세요.

배열 (Arrays)

[1, 2, 3] 같은 배열의 타입을 지정할 때는 number[]처럼 쓰면 돼요. 이 문법은 어떤 타입에도 똑같이 적용돼요. 예를 들어 string[]은 문자열 배열이에요. Array<number>라고 쓰는 걸 볼 때도 있는데, 의미는 완전히 같아요. T<U> 형태의 문법은 *제네릭(generics)*을 다룰 때 더 자세히 볼게요.

참고로 [number]는 완전히 다른 의미예요. 이건 튜플(Tuples) 부분에서 다룰게요.

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가 맥락(context)에서 추론할 수도 없으면, 컴파일러는 보통 any로 처리해요.

다만 any는 타입 체크가 되지 않으니 보통 피하는 게 좋아요. 컴파일러 플래그 noImplicitAny를 켜면 암시적으로 생긴 any를 에러로 표시해줘요.

변수의 타입 주석 (Type Annotations on Variables)

const, var, let으로 변수를 선언할 때, 변수의 타입을 명시적으로 지정하는 타입 주석을 선택적으로 붙일 수 있어요:

let myName: string = "Alice";
//        ^^^^^^^^ Type annotation

TypeScript는 int x = 0;처럼 "왼쪽에 타입이 오는" 선언을 쓰지 않아요. 타입 주석은 항상 타입을 붙이는 대상 뒤에 옵니다.

다만 대부분의 경우 이 주석은 꼭 필요하지 않아요. 가능하면 TypeScript는 코드에서 타입을 자동으로 *추론(infer)*하려고 해요. 예를 들어 변수의 타입은 초깃값의 타입에 따라 추론돼요:

// 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() + "!!");
}

매개변수에 타입 주석이 있으면, 그 함수에 전달되는 인자(argument)는 검사받아요:

// Would be a runtime error if executed!
greet(42);

매개변수에 타입 주석을 안 붙여도, TypeScript는 인자를 올바른 개수로 넘겼는지는 여전히 검사해요.

반환 타입 주석 (Return Type Annotations)

반환 타입 주석도 붙일 수 있어요. 반환 타입 주석은 매개변수 목록 뒤에 옵니다:

function getFavoriteNumber(): number {
  //                        ^^^^^^^^
  return 26;
}

변수 타입 주석과 마찬가지로, 반환 타입 주석도 보통은 안 붙여도 돼요. TypeScript가 return 문을 보고 함수의 반환 타입을 추론하거든요. 위 예시의 타입 주석은 사실 아무것도 바꾸지 않아요. 다만 일부 코드베이스에서는 문서화를 위해, 혹은 실수로 바뀌는 걸 막기 위해, 아니면 그냥 취향에 따라 반환 타입을 명시하기도 해요.

프로미스를 반환하는 함수 (Functions Which Return Promises)

프로미스를 반환하는 함수의 반환 타입을 주석으로 달고 싶다면 Promise 타입을 쓰면 돼요:

async function getFavoriteNumber(): Promise<number> {
  return 26;
}

익명 함수 (Anonymous Functions)

익명 함수는 함수 선언과는 조금 달라요. 함수가 TypeScript가 "이 함수가 어떻게 호출될지"를 알 수 있는 자리에 등장하면, 그 함수의 매개변수에 타입이 자동으로 부여돼요.

예시를 볼게요:

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)*이라고 불러요. 함수가 놓인 *문맥(context)*이 그 함수가 가져야 할 타입을 알려주기 때문이에요.

추론 규칙처럼 이 과정을 자세히 외울 필요는 없어요. 다만 이런 일이 일어난다는 것을 아는 것만으로도, 어디에 타입 주석이 필요 없는지 알아차리는 데 도움이 돼요. 나중에 값이 놓인 문맥이 그 타입에 영향을 주는 예시를 더 볼 거예요.

객체 타입 (Object Types)

원시 타입 다음으로 가장 많이 마주치는 타입은 *객체 타입(object type)*이에요. 속성(property)을 가진 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 });

여기서 매개변수의 타입에 xy 두 속성이 있고, 둘 다 number 타입이라고 주석을 달았어요. 속성을 구분할 때는 ,;를 쓸 수 있고, 마지막 구분자는 어느 쪽이든 생략 가능해요.

각 속성의 타입 부분도 선택이에요. 타입을 지정하지 않으면 any로 간주돼요.

선택적 속성 (Optional Properties)

객체 타입은 속성 중 일부(또는 전부)를 *선택적(optional)*으로 만들 수 있어요. 속성 이름 뒤에 ?를 붙이면 돼요:

function printName(obj: { first: string; last?: string }) {
  // ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

JavaScript에서는 존재하지 않는 속성에 접근해도 런타임 에러가 나지 않고 undefined 값을 돌려받아요. 그래서 선택적 속성을 읽을 때는, 그 값을 쓰기 전에 undefined인지 확인해야 해요:

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의 타입 시스템은 다양한 연산자를 써서 이미 있는 타입들로 새 타입을 만들어낼 수 있게 해줘요. 몇 가지 타입을 쓰는 법을 알았으니, 이제 이 타입들을 조합해 보는 시간이에요.

유니언 타입 정의하기 (Defining a Union Type)

타입을 조합하는 첫 번째 방법으로 만나는 게 유니언(union) 타입이에요. 유니언 타입은 두 개 이상의 타입으로 만들어진 타입으로, 그중 어느 하나일 수 있는 값들을 나타내요. 유니언을 이루는 각 타입을 유니언의 *멤버(member)*라고 불러요.

문자열이나 숫자에 모두 동작하는 함수를 하나 만들어 볼게요:

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);
}

유니언 타입 다루기 (Working with Union Types)

유니언 타입에 맞는 값을 주는 건 쉬워요. 유니언의 어떤 멤버든 그중 하나에 맞는 타입만 제공하면 되거든요. 그럼 반대로 유니언 타입의 값을 가지고 있을 때는 어떻게 다뤄야 할까요?

TypeScript는 유니언의 모든 멤버에 대해 유효한 연산만 허용해요. 예를 들어 string | number 유니언이 있으면, string에만 있는 메서드는 쓸 수 없어요:

function printId(id: number | string) {
  console.log(id.toUpperCase());
}

해결 방법은 JavaScript에서 타입 주석 없이 하듯이, 코드로 유니언을 좁히는(narrow) 거예요. *내로잉(narrowing)*은 코드의 구조를 보고 TypeScript가 값에 대해 더 구체적인 타입을 추론할 수 있을 때 일어나요.

예를 들어 TypeScript는 typeof 값이 "string"인 값은 오직 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 분기에서는 특별히 할 게 없다는 점을 눈여겨보세요. xstring[]이 아니라면 string일 수밖에 없으니까요.

때로는 유니언의 모든 멤버가 공통된 무언가를 가질 때도 있어요. 예를 들어 배열과 문자열은 둘 다 slice 메서드를 가져요. 유니언의 모든 멤버가 공통 속성을 가지면, 내로잉 없이도 그 속성을 쓸 수 있어요:

// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3);
}

이상하게 들릴 수 있는데요, 타입의 유니언이 그 타입들의 속성의 *교집합(intersection)*을 가진 것처럼 보여요. 이것은 우연이 아니에요. 유니언이라는 이름 자체가 타입 이론에서 온 거예요. number | string 유니언은 각 타입 값들의 합집합을 취해서 만들어진답니다. 두 집합이 있고 각 집합에 대한 사실이 있다고 할 때, 그 사실들의 교집합만이 두 집합의 합집합에도 적용돼요. 예를 들어 모자를 쓴 키 큰 사람들이 있는 방과, 모자를 쓴 스페인어 사용자들이 있는 방이 있을 때, 두 방을 합친 뒤 모든 사람에 대해 확실히 알 수 있는 건 오직 "모자를 쓰고 있다"는 것뿐이에요.

타입 별칭 (Type Aliases)

지금까지 객체 타입과 유니언 타입을 타입 주석에 직접 써서 썼어요. 편리하긴 한데, 같은 타입을 두 번 이상 쓰고 싶고 이름으로 참조하고 싶을 때가 흔해요.

*타입 별칭(type alias)*이 바로 그거예요. 어떤 타입에 붙이는 이름이죠. 타입 별칭의 문법은 이렇게 돼요:

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;
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 declaration)*은 객체 타입에 이름을 붙이는 또 다른 방법이에요:

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를 구조적 타입(structurally typed) 시스템이라고 불러요.

타입 별칭과 인터페이스의 차이 (Differences Between Type Aliases and Interfaces)

타입 별칭과 인터페이스는 아주 비슷하고, 많은 경우 자유롭게 둘 중 하나를 골라 쓸 수 있어요. interface의 거의 모든 기능이 type에서도 쓸 수 있는데, 핵심 차이는 이거예요. 타입 별칭은 새 속성을 추가하려고 다시 열 수 없지만, 인터페이스는 언제든 확장할 수 있어요.

인터페이스 확장하기 (Extending an interface)

interface Animal {
  name: string;
}

interface Bear extends Animal {
  honey: boolean;
}

const bear = getBear();
bear.name;
bear.honey;

교집합(intersection)으로 타입 확장하기 (Extending a type via intersections)

type Animal = {
  name: string;
}

type Bear = Animal & { 
  honey: boolean;
}

const bear = getBear();
bear.name;
bear.honey;

기존 인터페이스에 새 필드 추가하기 (Adding new fields to an existing interface)

interface Window {
  title: string;
}

interface Window {
  ts: TypeScriptAPI;
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});

타입은 만든 뒤에는 바꿀 수 없어요 (A type cannot be changed after being created)

type Window = {
  title: string;
}

type Window = {
  ts: TypeScriptAPI;
}

 // Error: Duplicate identifier 'Window'.

이 개념들은 뒤 장들에서 더 자세히 배우게 될 테니, 지금 다 이해가 안 되더라도 걱정하지 마세요.

또 몇 가지 알아둘 점이 있어요:

대체로 취향에 따라 고르면 되고, TypeScript가 다른 종류의 선언이 필요하다면 알려줄 거예요. 판단 기준이 필요하다면, type의 기능이 필요해질 때까지 interface를 쓰는 걸 권장해요.

타입 단언 (Type Assertions)

때로는 TypeScript가 알 수 없는 값의 타입 정보를 여러분이 알고 있을 때가 있어요.

예를 들어 document.getElementById를 쓰면 TypeScript는 어떤 HTMLElement를 반환한다는 것만 알 수 있어요. 하지만 여러분은 그 페이지에 주어진 ID를 가진 HTMLCanvasElement가 항상 있다는 걸 알고 있을 수 있죠.

이런 상황에서는 *타입 단언(type assertion)*을 써서 더 구체적인 타입을 지정할 수 있어요:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

타입 주석처럼 타입 단언도 컴파일러에 의해 제거되고, 코드의 런타임 동작에는 영향을 주지 않아요.

등가인 꺾쇠괄호(angle-bracket) 문법도 쓸 수 있어요(단, 코드가 .tsx 파일에 있으면 안 돼요):

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

주의하세요: 타입 단언은 컴파일 타임에 제거되기 때문에, 타입 단언과 관련된 런타임 검사는 없어요. 타입 단언이 틀렸다고 해서 예외나 null이 생기지 않아요.

TypeScript는 어떤 타입의 더 구체적인 버전 또는 덜 구체적인 버전으로 변환하는 타입 단언만 허용해요. 이 규칙이 stringnumber로 바꾸는 같은 "불가능한" 강제 변환을 막아주죠:

const x = "hello" as number;

이 규칙이 너무 보수적이어서, 유효할 수도 있는 더 복잡한 변환까지 막을 때도 있어요. 그럴 땐 단언을 두 번 쓰면 돼요. 먼저 any(또는 나중에 소개할 unknown)로 바꾼 다음, 원하는 타입으로 바꾸는 거예요:

declare const expr: any;
type T = { a: 1; b: 2; c: 3 };
const a = expr as any as T;

리터럴 타입 (Literal Types)

일반적인 stringnumber 타입 말고도, 타입이 오는 자리에 특정한 문자열과 숫자를 가리킬 수 있어요.

이걸 생각하는 쉬운 방법 하나는 JavaScript에서 변수를 선언하는 여러 방법을 떠올리는 거예요. varlet은 변수 안에 담긴 값을 바꿀 수 있지만, 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;

리터럴 타입만으로는 별로 쓸모가 없어요:

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";

값을 딱 하나만 담을 수 있는 변수가 무슨 소용이겠어요!

하지만 리터럴을 유니언으로 조합하면 훨씬 유용한 개념을 표현할 수 있어요. 예를 들어 특정한 알려진 값의 집합만 받는 함수 같은 거요:

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;
}

물론 이걸 리터럴이 아닌 타입과도 조합할 수 있어요:

interface Options {
  width: number;
}
function configure(x: Options | "auto") {
  // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");

리터럴 타입에는 한 가지 종류가 더 있어요. 바로 불리언 리터럴이에요. 불리언 리터럴 타입은 두 개뿐이고, 짐작하셨다시피 truefalse예요. boolean 타입 자체는 사실 true | false 유니언의 별칭일 뿐이에요.

리터럴 추론 (Literal Inference)

객체로 변수를 초기화하면 TypeScript는 그 객체의 속성들이 나중에 값이 바뀔 수 있다고 가정해요. 예를 들어 이런 코드를 썼다고 해볼게요:

declare const someCondition: boolean;
const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

TypeScript는 이전에 0이던 필드에 1을 할당하는 걸 에러로 보지 않아요. 다른 말로 하면, obj.counter0이 아니라 number 타입이어야 해요. 타입이 읽기쓰기 동작을 모두 결정하는 데 쓰이기 때문이에요.

문자열도 마찬가지예요:

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 호출 사이에 "GUESS" 같은 새 문자열을 req.method에 할당할 수 있는 코드가 실행될 수 있으니까, TypeScript는 이 코드를 에러로 보는 거예요.

이 문제를 우회하는 방법이 둘 있어요.

  1. 어느 한 곳에 타입 단언을 추가해서 추론을 바꿀 수 있어요:

    declare function handleRequest(url: string, method: "GET" | "POST"): void;
    // 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"임을 알고 있다"는 뜻이에요.

  2. as const를 써서 객체 전체를 리터럴 타입으로 바꿀 수 있어요:

    declare function handleRequest(url: string, method: "GET" | "POST"): void;
    const req = { url: "https://example.com", method: "GET" } as const;
    handleRequest(req.url, req.method);
    

as const 접미사는 타입 시스템에서 const처럼 동작해요. 모든 속성이 string이나 number 같은 더 일반적인 버전 대신 리터럴 타입으로 지정되도록 보장하죠.

nullundefined

JavaScript에는 없거나 초기화되지 않은 값을 나타내는 두 원시 값이 있어요. nullundefined죠.

TypeScript에도 같은 이름의 대응하는 타입 두 개가 있어요. 이 타입들이 어떻게 동작하는지는 strictNullChecks 옵션이 켜져 있느냐에 달려 있어요.

strictNullChecks 꺼짐

strictNullChecks꺼져 있으면, null이나 undefined일 수 있는 값도 평소처럼 접근할 수 있고, nullundefined 값을 어떤 타입의 속성에도 할당할 수 있어요. null 검사가 없는 언어(예: C#, Java)처럼 동작하죠. 이런 값들에 대한 검사가 없다는 건 버그의 주요 원인이 되기 쉬워요. 코드베이스에서 가능하다면 언제나 strictNullChecks를 켜는 걸 권장해요.

strictNullChecks 켜짐

strictNullChecks켜져 있으면, 값이 null이나 undefined일 때 그 값의 메서드나 속성을 쓰기 전에 먼저 그 값들을 검사해야 해요. 선택적 속성을 쓰기 전에 undefined를 확인했던 것처럼, *내로잉(narrowing)*을 써서 null일 수 있는 값을 검사할 수 있어요:

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

널 아님 단언 연산자 (Non-null Assertion Operator, 접미사 !)

TypeScript에는 명시적인 검사 없이 타입에서 nullundefined를 제거하는 특별한 문법도 있어요. 어떤 표현식 뒤에 !를 쓰는 건, 그 값이 null이나 undefined가 아니라고 단언(assert)하는 것과 같아요:

function liveDangerously(x?: number | null) {
  // No error
  console.log(x!.toFixed());
}

다른 타입 단언처럼 이 연산자도 코드의 런타임 동작을 바꾸지 않아요. 그래서 !는 그 값이 null이나 undefined일 수 없다는 것을 확실히 알 때만 쓰는 게 중요해요.

열거형 (Enums)

열거형(enum)은 TypeScript가 JavaScript에 추가한 기능으로, 가능한 이름 붙은 상수 집합 중 하나일 수 있는 값을 표현해요. 대부분의 TypeScript 기능과 달리 이건 타입 수준의 추가가 아니라, 언어와 런타임 자체에 더해지는 거예요. 그래서 이런 게 존재한다는 건 알아두되, 확실하지 않다면 쓰는 걸 미뤄두는 게 좋아요. 열거형에 대해 더 자세히 알고 싶으면 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

JavaScript에는 Symbol() 함수로 전역에서 유일한 참조를 만드는 원시 타입이 있어요:

const firstName = Symbol("name");
const secondName = Symbol("name");

if (firstName === secondName) {
  // Can't ever happen
}

이것들에 대해 더 알아보고 싶다면 Symbols 참조 페이지를 읽어보세요.

더 알아보기