TypeScript와 같은 상위 집합 언어에 특정 새로운 런타임 기능으로 JavaScript 구문을 확장하는 방식은 다음과 같은 이유로 나쁜 사례로 간주합니다.
따라서 초기 TypeScript 설계자들이 TypeScript 언어로 JavaScript의 세 가지 구문 확장을 도입했다는 것이 유감스럽습니다.
클래스
열거형 (enum)
네임스페이스 (namespace)
TypeScript는 decorator에 대한 실험적인 제안(experimental proposal)을 채택했습니다.
Tip
- 클래스를 많이 사용하는 프로젝트나 클래스 이점을 갖는 프레임워크가 아니라면 클래스 매개변수 속성을 사용하지 않는 것이 좋습니다.
class Engineer {
constructor(readonly area: string) {
console.log(`I work in the ${area} area.`);
}
}
// 타입 : string
new Engineer("mechanical").area;
class NamedEngineer {
fullName: string;
constructor(
name: string,
public area: string
) {
this.fullName = `${name}, ${area} engineer`;
}
}
대부분의 프로젝트는 앞에서 언급했던 단점으로 인해 매개변수 속성을 완전히 사용하지 않는 것을 선호합니다.
반면에 클래스 생성을 매우 선호하는 프로젝트에서는 매개변수 속성을 사용하면 정말 좋습니다.
Tip
- ECMAScript 버전이 decorator 구분으로 승인될 때까지 가능하면 decorator를 사용하지 않는 것이 좋습니다.
- TypeScript decorator 사용을 권장하는 Angular나 Nest.JS와 같은 프레임워크 버전에서 작업하는 경우 프레임워크 설명서에서 decorator를 사용하는 방법을 알려줍니다.
@myDecorator
class MyClass { /* ... */ }
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true
}
}
function logOnCall(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
console.log('[logOnCall] I am decorating', target.constructor.name);
descriptor.value = function (...args: unknown[]) {
console.log(`[descriptor.value] calling '${key}' with:`, ...args);
}
}
class Greeter {
@logOnCall
greet(message: string) {
console.log(`[greet] Hello, ${messgage}`);
}
}
new Greeter().greet("you");
// Output log:
// "[logOnCall] I am decorating", "Greeter"
// "[descriptor.value] Calling 'greet' with:", "you"
// "[greet] Hello, you"
Tip
- 자주 반복되는 리터럴 집합이 있고, 그 리러털 집합을 공통 이름으로 설명할 수 있으며, enum으로 전환했을 때 훨씬 더 읽기 쉬운 경우에만 enum을 사용합니다.
const StatusCodes = {
InternalServerError: 500,
NotFound: 404,
Ok: 200,
// ...
} as const;
StatusCodes.InternalServerError; // 500
// 타입 : 200 | 404 | 500
type StatusCodeValue = (typeof StatusCodes)[keyof typeof StatusCodes];
let statusCodeValue : StatusCodeValue;
// Ok
statusCodeValue = 200;
// Error: Type '-1' is not assignable to 'StatusCodeValue'.
statusCodeValue = -1;
enum StatusCode {
InternalServerError = 500,
NotFound = 404,
Ok = 200,
}
StatusCode.InternalServerError; // 500
let statusCode: StatusCode;
statusCode = StatusCode.Ok; // Ok
statusCode = 200; // Ok
var StatusCode;
(function (StatusCode) {
StatusCode[StatusCode["InternalServerError"] = 500] = "InternalServerError";
StatusCode[StatusCode["NotFound"] = 404] = "NotFound";
StatusCode[StatusCode["Ok"] = 200] = "Ok";
})(StatusCode || (StatusCode = {}));
StatusCode.InternalServerError;
enum VisualTheme {
Dark, // 0
Light, // 1
System, // 2
}
var VisualTheme;
(function (VisualTheme) {
VisualTheme[VisualTheme["Dark"] = 0] = "Dark";
VisualTheme[VisualTheme["Light"] = 1] = "Light";
VisualTheme[VisualTheme["System"] = 2] = "System";
})(VisualTheme || (VisualTheme = {}));
enum Direction {
Top = 1,
Right,
Bottom,
Left,
}
var Direction;
(function (Direction) {
Direction[Direction["Top"] = 1] = "Top";
Direction[Direction["Right"] = 2] = "Right";
Direction[Direction["Bottom"] = 3] = "Bottom";
Direction[Direction["Left"] = 4] = "Left";
})(Direction || (Direction = {}));
Tip
- enum의 순서를 수정하면 기본 번호가 변경됩니다.
- 만약에 데이터베이스 같은 곳에 enum 값을 저장하고 있다면 enum 순서를 변경하거나 항목을 제거할 때 주의해야 합니다.
- 저장된 번호가 더 이상 코드가 예상한 것과 같지 않기 때문에 데이터가 갑자기 손상될 수 있습니다.
enum LoadStyle {
AsNeed = "as-needed",
Eager = "eager",
}
var LoadStyle;
(function (LoadStyle) {
LoadStyle["AsNeed"] = "as-needed";
LoadStyle["Eager"] = "eager";
})(LoadStyle || (LoadStyle = {}));
문자열값을 갖는 enum은 읽기 쉬운 이름으로 공유 상수의 별칭을 지정하는 데 유용합니다.
문자열값의 한 가지 단점은 TypeScript에 따라 자동으로 계산할 수 없다는 것입니다.
enum Wat {
FristString = "first",
SomeNumber = 9000,
ImplicitNumber, // Ok (value 9001)
AnotherString = "another",
NotAllowed, // Error: Enum member must have initializer.
}
Tip
- 이론적으로 숫자와 문자열 모두 멤버로 갖는 enum을 만들 수 있습니다.
- 하지만 실제로 이런 형태의 enum은 불필요하고 혼란스러울 수 있으므로 사용해서는 안됩니다.
const enum DisplayHint {
Opaque = 0,
Semitransparent,
Transparent,
}
let displayHint = DisplayHint.Transparent;
let displayHint = 2 /* DisplayHint.Transparent */;
var DisplayHint;
(function (DisplayHint) {
DisplayHint[DisplayHint["Opaque"] = 0] = "Opaque";
DisplayHint[DisplayHint["Semitransparent"] = 1] = "Semitransparent";
DisplayHint[DisplayHint["Transparent"] = 2] = "Transparent";
})(DisplayHint || (DisplayHint = {}));
let displayHint = 2 /* DisplayHint.Transparent */;
warning
- 기존 패키지에 대한 DefinitelyTyped 타입 정의를 작성하지 않는 한 namespace를 사용하지 마세요.
- namespace는 최신 JavaScript Module 의미 체계와 일치하지 않습니다.
- 자동 멤버 할당은 코드를 읽는 것을 혼란스럽게 만들 수 있습니다.
- .d.ts 파일에서 namespace를 접할 수 있습니다.
ECMAScript Module이 승인되기 전에 웹 애플리케이션이 출력 코드 대부분을 브라우저에 따라 로드되는 하나의 파일로 묶는 것이 일반적이었습니다.
TypeScript 언어는 namespace라 부르는 내부 모듈(internal module) 개념을 가진 하나의 해결책을 제공했습니다.
namespace Randomized {
const value = Math.random();
console.log(`My value is ${value}`);
}
var Randomized;
(function (Randomized) {
const value = Math.random();
console.log(`My value is ${value}`);
})(Randomized || (Randomized = {}));
namespace Settings {
export const name = "My Application";
export const version = "1.2.3";
export function describe() {
return `${Settings.name} at version ${Settings.version}`;
}
console.log("Initializing", describe());
}
console.log("Ininitialized", Settings.describe());
var Settings;
(function (Settings) {
Settings.name = "My Application";
Settings.version = "1.2.3";
function describe() {
return `${Settings.name} at version ${Settings.version}`;
}
Settings.describe = describe;
console.log("Initializing", describe());
})(Settings || (Settings = {}));
console.log("Ininitialized", Settings.describe());
// settings/constants.ts
namespace Settings {
export const name = "My Application";
export const version = "1.2.3";
}
// settings/describe.ts
namespace Settings {
export function describe() {
return `${Settings.name} at version ${Settings.version}`;
}
console.log("Initializing", describe());
}
// index.ts
console.log("Ininitialized", Settings.describe());
// settings/constants.ts
var Settings;
(function (Settings) {
Settings.name = "My Application";
Settings.version = "1.2.3";
})(Settings || (Settings = {}));
// settings/describe.ts
(function (Settings) {
function describe() {
return `${Settings.name} at version ${Settings.version}`;
}
Settings.describe = describe;
console.log("Initializing", describe());
})(Settings || (Settings = {}));
console.log("Ininitialized", Settings.describe());
const Settings = {
describe: function describe() {
return `${Settings.name} at version ${Settings.version}`;
},
name: "My Application",
version: "1.2.3",
}
namespace는 다른 namespace 내에서 namespace를 내보내거나 하나 이상의 마침표(.)를 사용해서 무한으로 중첩할 수 있습니다.
다음 두 개의 namespace 선언은 동일하게 작동합니다.
namespace Root.Nested {
export const value1 = true;
}
namespace Root {
export namespace Nested {
export const value2 = true;
}
}
(function (Root) {
var Nested;
(function (Nested) {
Nested.value1 = true;
})(Nested = Root.Nested || (Root.Nested = {}));
})(Root || (Root = {}));
namespace는 DefinitelyTyped 타입 정의에 유용합니다.
<script>
태그를 사용해 웹 브라우저에 포함되도록 설정합니다.또한 많은 브라우저 지원 JavaScript 라이브러리는 더 현대적인 모듈 시스템에 삽입되고 전역 namespace를 생성하기 위해 설정됩니다.
// node_modules/@types/my-example-lib/index.d.ts
export const value: number;
export as namespace libExample;
// src/index.ts
import * as libExample from "my-example-lib"; // Ok
const value = window.libExample.value; // Ok
// settings/constants.ts
export const name = "My Application";
export const version = "1.2.3";
// settings/describe.ts
import { name, version } from "./constants";
export function describe() {
return `${name} at version ${version}`;
}
console.log("Initializing", describe());
// index.ts
import { describe } from "./settings/describe";
console.log("Initialized", describe());
타입 전용 가져오기(import)와 내보내기(export)는 매우 유용하며 내보내진 JavaScript 출력에 어떠한 복잡성도 추가하지 않습니다.
TypeScript의 트랜스파일러는 JavaScript 런타임에 사용되지 않으므로 파일의 가져오기와 내보내기에서 타입 시스템에서만 사용되는 값을 제거합니다.
예를 들어 다음 index.ts 파일은 action 변수와 ActivistArea 타입을 생성한 다음, 나중에 독립형 내보내기 선언을 사용해서 두 개를 모두 보냅니다.
// index.ts
const action = { area: "people", name: "Bella Abzug", role: "politician" };
type ActivistArea = "nature" | "people";
export { action, ActivistArea };
// index.js
const action = { area: "people", name: "Bella Abzug", role: "politician" };
export { action };
다시 내보낸 타입을 제거하려면 TypeScript 타입 시스템에 대한 지식이 필요합니다.
한 번에 하나의 파일을 작동하는 Babel 같은 트랜스파일러는 각 이름이 타입 시스템에서만 사용되는지 여부를 알 수 있는 TypeScript 타입 시스템에 접근할 수 없습니다.
TypeScript는 export와 import 선언에서 개별적으로 가져온 이름 또는 전체 { ... } 객체 앞에 type 제한자를 추가할 수 있습니다.
// index.ts
import { type TypeOne, value} from "my-example-types";
import type { TypeTwo } from "my-example-types";
import type DefaultType from "my-example-types";
export { type TypeOne, value };
export type { DefaultType, TypeTwo };
// index.js
import { value } from "my-example-types";
export { value };
import { ClassOne, type ClassTwo } from "my-example-types";
// Ok
new ClassOne();
// Error: 'ClassTwo' cannot be used as a value because it was imported using 'import type'.
new ClassTwo();