TypeScript, 타입으로써 {}object의 차이점

타입으로써 {}object를 구분하는 것은 타입스크립트 프로그래머로써 매우 매우 중요합니다.

먼저 {} 타입으로 정의된 변수 객체에는 nullundefined를 제외한 모든 것들을 할당할 수 있습니다. object 타입으로 정의된 변수 객체에는 원시타입 값을 제외한 모든 것들을 할당할 수 있습니다.

즉, object 타입으로 선언된 아래의 예제 코드를 참고하기 바랍니다.

let obj: object;

obj = {};
obj = [];
obj = new Date();
obj = () => {};
obj = /abc/;

obj = 10;          // 오류
obj = "hello";     // 오류
obj = true;        // 오류
obj = Symbol();    // 오류
obj = BigInt(10);  // 오류

{} 타입으로 선언된 아래의 예제 코드도 있습니다.

let x: {};

x = {};
x = [];
x = 10;
x = "hello";
x = true;
x = () => {};

x = null;          // 오류
x = undefined;     // 오류

TypeScript, 메타 프로그래밍 …

TypeScript는 코드에 대한 코드 작성을 목적으로 하는 메타 프로그래밍 언어입니다. 현존하는 대다수의 거의 대부분의 프로그래밍 언어가 메타 프로그래밍이 가능하지만, 특히 TypeScript는 코드에서 타입을 자동으로 새롭게 재정의가 가능하다는 점에서 다른 프로그래밍 언어보다 조금은 더 메타 프로그래밍 언어적이라고 할 수 있습니다.

타입스크립트에서 메타 프로그래밍을 위해 제공하는 기능은 Generic, keyof, typeof, Conditional Types, Mapped Types, Template Literal Types 등이 있고 서브셋은 JavaScript의 메타 프로그래밍 기능인 Proxy, Reflect, Decorator(제안), Object.defineProperty() 등입니다. 타입스크립트에 한정하여 볼때 아래는 메타 프로그래밍을 위해 제공하는 아래의 예제 코드입니다.

// Generic
type Box<T> = { value: T };

// keyof
type Keys = keyof T;

// typeof
type T = typeof value;

// Indexed Access Type
type Value = T[K];

// Mapped Type
type Readonly<T> = {
    readonly [K in keyof T]: T[K];
};

// Conditional Type
type Result<T> = T extends string ? number : boolean;

// Template Literal Type
type Event<T extends string> = `${T}Changed`;

// infer
type Return<T> = T extends (...args: any[]) => infer R ? R : never;

TypeScript, 열거 타입은 열거형보다 객체 방식이 보다 권장됨

열거 타입을 정의하기 위해 TypeScript는 열거형을 제공합니다. 예를들어 다음 코드가 가능합니다.

const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}

function walk(dir: EDirection) {}
walk(EDirection.Left);

위의 코드와 비슷한 맥락으로, 이번에는 열거형을 사용하지 않고 객체 방식으로 작성하면 다음과 같습니다.

const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;

type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
run(ODirection.Right);

TypeScript, {}

{}는 빈 객체입니다. 하지만 TypeScript에서는 nullundefined를 제외한 모든 값을 의미하는 특수한 타입이기도 합니다.

그래서 다음 코드가 있다고 한다면 …

type A = {}
let a: A;

다음 코드는 모두 문제가 없습니다.

a = "hello";
a = 123;
a = true;
a = [];
a = {};
a = () => {};

그러나 다음 코드는 에러입니다.

a = null;
a = undefined;

그럼 {} 타입은 어디에 쓰는 물건일까요? 바로 NonNullable 입니다. NonNullable은 다음처럼 작성되어져 있습니다.

type NonNullable<T> = T & {}

NonNullable은 타입에서 nullundefined를 제외시킨 타입을 만들어줍니다. 아래코드처럼요.

type T0 = NonNullable<string | number | undefined>;
// type T0 = string | number

type T1 = NonNullable<string[] | null | undefined>;
// type T1 = string[]

NonNullable의 구현은 TypeScript의 버전에 따라 다른데요. 좀더 오래된 4.8에서는 다음처럼 구현되어 있다는 점도 알아두시면 좋습니다.

type NonNullable<T> =
    T extends null | undefined
        ? never
        : T;

TypeScript, keyof any ?

타입스크립트로 작성된 코드를 보면 keyof any가 무엇인가… 라는 의문이 생깁니다. 결론부터 이야기하면 string | number | symbol 타입입니다. 즉, 객체를 구성하는 키가 가질 수 있는 타입입니다. 다음 코드를 보겠습니다.

type Person = {
    name: string;
    age: number;
};

type K = keyof Person;

K"name" | "age" 타입입니다. 즉, Person 객체의 key 값으로 구성된 유니언 타입입니다. 이러한 keyof의 작동을 봤을때 keyof any는 객체의 key 값으로 구성되는 타입입니다. 즉, 앞서 봤던 string | number | symbol이 됩니다. Person의 경우 키 값의 타입은 string 뿐입니다. 배열은 키 값의 타입으로 number를 갖습니다. 특수한 경우 키값의 타입으로 symbol을 가질 수도 있구요. 그래서 객체를 구성하는 키가 가질 수 있는 타입은 string | number | symbol 인 것입니다.

keyof any가 객체의 키 타입이라는 것이라는 것을 즉시 유추하기는 일반적으로 어렵습니다. 그래서 TypeScript는 keyof any와 동일한 타입으로 다음과 같은 PropertyKey 타입을 미리 정의두고 있습니다.

type PropertyKey = string | number | symbol;