클래스 선언은 세미콜론으로 끝나서는 안 된다:
class Baz {
method(): number {
return this.x;
}
}
class Foo {
method(): number {
return this.x;
}
} // 공백 스타일은 선택 가능
클래스 표현식을 포함하는 명령문은 세미콜론으로 끝나야 한다:
export const Baz = class extends Bar {
method(): number {
return this.x;
}
};
클래스 메서드 선언에서는 세미콜론을 사용하지 않는다:
Before
class Foo {
doThing() {
console.log("A");
}; // <-- unnecessary
}
After
class Foo {
doThing() {
console.log("A");
}
}
메서드 선언은 주변 코드와 단일 빈 줄로 구분되어야 한다:
Before
class Foo {
doThing() {
console.log("A");
}
getOtherThing(): number {
return 4;
}
}
After
class Foo {
doThing() {
console.log("A");
}
getOtherThing(): number {
return 4;
}
}
toString
메서드는 디버깅과 로깅에서 자주 호출되므로 안정적이고 예외가 없는 방식으로 동작해야 한다:
부작용을 유발하거나 무한 루프, 예외 발생 가능성을 초래하는 코드는 피한다.
가능한 단순하게 구현하되, 객체 상태를 적절히 표현하는 문자열을 반환해야 한다.
private
정적 메서드(static methods)는 지양하자:
private
정적 메서드
클래스 내부에서만 호출할 수 있다. 클래스 외부나 서브클래스에서는 접근할 수 없다. 즉, 해당 메서드는 클래스 자신이 정의된 곳에서만 사용 가능한 보조적인 기능을 제공한다.
일반 정적 메서드 (예: public
, protected
)
클래스 외부에서도 호출할 수 있다. public
정적 메서드는 어디서든 접근할 수 있고, protected
정적 메서드는 서브클래스에서만 접근할 수 있다.
모듈 로컬 함수란?
모듈 로컬 함수는 클래스나 객체의 메서드가 아니라, 파일 단위로 정의된 함수로, 해당 모듈 내에서만 사용할 수 있다.
클래스와 무관하게 독립적인 로직을 처리할 수 있으므로 다른 클래스나 모듈에서 쉽게 가져다 쓸 수 있다.
`private`` 정적 메서드를 피해야 하는 이유
private
정적 메서드는 클래스의 주요 책임과 관련이 없는 보조적인 역할을 할 때가 많아, 클래스를 더 복잡하게 만든다.private
정적 메서드는 클래스의 내부에 종속되어 있어 단위 테스트가 어려울 수 있다.동적 디스패치에 의존하지 말라:
정적 메서드는 해당 메서드를 직접 정의한 기본 클래스에서만 호출해야 한다.
동적 디스패치(Dynamic Dispatch)란?
메서드 호출 시점에 메서드가 어떤 클래스의 메서드인지 동적으로 결정되는 방식을 말한다.
이는 주로 다형성(Polymorphism)을 다룰 때 사용되지만, 정적 메서드 호출에서는 예상치 못한 동작을 초래할 수 있다.
서브클래스에서 정의되지 않은 메서드 호출
class Base {
/** @nocollapse */ static foo() {}
}
class Sub extends Base {}
// BAD: 정적 메서드를 동적으로 호출
function callFoo(cls: typeof Base) {
cls.foo();
}
// BAD: 서브클래스에서 정의되지 않은 정적 메서드 호출
Sub.foo(); // 이는 허용되지 않음
@nocollapse
데코레이터는 Closure Compiler에 의해 정적 메서드나 필드를 클래스 정의에서 분리하지 말라는 지시이다. 이로 인해 정적 메서드가 올바르게 상속되고, 동적 호출이 가능해진다.
정적 메서드에서 this
접근을 피해야 한다:
정적 필드가 상속되고, 서브클래스에서 재정의될 수 있다는 점은 명확하지 않으며, 실수로 잘못된 동작을 초래할 수 있다.
항상 클래스 이름을 명시적으로 사용하여 정적 필드나 메서드에 접근하자.
정적 상태를 최소화하고, 가능한 한 인스턴스 상태를 활용해 코드의 격리성을 높이자.
Before
class ShoeStore {
static storage: Storage = ...;
static isAvailable(s: Shoe) {
return this.storage.has(s.id); // this.storage는 ShoeStore를 참조
}
}
class EmptyShoeStore extends ShoeStore {
static storage: Storage = EMPTY_STORE;
}
EmptyShoeStore.isAvailable(s); // EMPTY_STORE
this.storage
는 EmptyShoeStore.storage
를 참조한다. 이 동작은 일반적으로 기대되지 않으며, 다른 언어에서는 유사한 패턴이 허용되지 않는 경우도 많다.After
class ShoeStore {
static storage: Storage = ...;
static isAvailable(s: Shoe) {
return ShoeStore.storage.has(s.id); // Good: 명시적으로 클래스 이름을 사용
}
}
class EmptyShoeStore extends ShoeStore {
static storage: Storage = EMPTY_STORE;
static isAvailable(s: Shoe) {
return EmptyShoeStore.storage.has(s.id); // Good: 명확하게 EmptyShoeStore를 참조
}
}
인수가 전달되지 않더라도 생성자 호출은 괄호를 사용해야 한다.
Before
const x = new Foo;
After
const x = new Foo();
괄호를 생략하면 미묘한 실수가 발생할 수 있다. 다음 두 줄은 동일하지가 않다.
new Foo().Bar(); new Foo.Bar();
ES2015에서는 기본 클래스 생성자를 제공하므로, 비어 있는 생성자나 부모 클래스에 위임하는 생성자를 제공할 필요가 없다. 생성자가 지정되지 않은 경우 기본 클래스 생성자가 제공된다.
Before
class UnnecessaryConstructor {
constructor() {}
}
After
class DefaultConstructor {
}
Before
class UnnecessaryConstructor {
constructor() {}
}
After
class DefaultConstructor {
}
그러나 매개변수 속성(parameter properties), 접근 제한자(modifier) 또는 매개변수 데코레이터(decorator)가 있는 생성자는 생성자의 본문이 비어 있더라도 생략해서는 안 된다.
class ParameterProperties {
constructor(private myService) {} // myService라는 속성을 클래스 내부에 생성하고, 생성자에서 초기화
}
class ParameterDecorators {
constructor(@SideEffectDecorator myService) {} //데코레이터를 사용해 매개변수에 부가적인 동작을 추가
}
class NoInstantiation {
private constructor() {} // 이 생성자는 private 접근 제어자를 사용해 클래스 외부에서의 인스턴스 생성을 막는다
}
생성자는 위와 아래의 주변 코드와 단일 빈 줄로 구분되어야 한다:
Before
class Foo {
myField = 10;
constructor(private readonly ctorParam) {}
doThing() {
console.log(ctorParam.getThing() + myField);
}
}
After
class Foo {
myField = 10;
constructor(private readonly ctorParam) {}
doThing() {
console.log(ctorParam.getThing() + myField);
}
}
#private
필드 사용금지:
#private
필드는 JavaScript의 최신 사양에 따른 문법으로, TypeScript가 ES2015 이하로 트랜스파일해야 할 경우 큰 문제가 된다. #private
를 ES2015 이하로 다운레벨(downlevel)할 때, TypeScript는 별도의 WeakMap 기반 코드를 생성한다. 결과적으로 출력되는 코드의 크기가 커지고 성능도 저하될 수 있다.#private
필드는 ES2015 이상에서만 동작하며, ES2015 이전 환경에서는 지원되지 않는다. 만약 프로젝트가 ES5나 ES3와 같은 더 오래된 환경을 대상으로 한다면 사용 불가.private
접근 제어자를 사용해 컴파일 타임에 필드 접근 제한을 강제할 수 있다. 런타임에서는 #private
와 동일한 수준의 보호를 제공하지 않더라도, 컴파일 시 확인으로 대부분의 개발 요구사항을 충족할 수 있다.class Clazz {
#ident = 1;
}
After
class Clazz {
private ident = 1;
}
생성자 외부에서 절대 다시 할당되지 않는 속성은 readonly
수정자를 권장한다.
readonly
키워드를 사용하면 필드가 생성자 외부에서 재할당되지 않음을 명시적으로 보장할 수 있다.
readonly
는 얕은 불변성(Shallow Immutability)만 제공한다. (예를 들어, 객체의 속성 자체는 변경 가능하다.)
파라미터 프로퍼티(Parameter properties) 사용:
파라미터 프로퍼티는 간결하면서도 TypeScript의 강력한 타입 시스템을 활용해 코드 품질을 높이는 방법이다.
Before
class Foo {
private readonly barService: BarService;
constructor(barService: BarService) {
this.barService = barService;
}
}
After
class Foo {
constructor(private readonly barService: BarService) {}
}
private
, protected
, public
, 또는 readonly
)를 추가해 멤버 변수를 자동으로 생성하고 초기화한다.만약 파라미터 프로퍼티에 설명이 필요한 경우 JSDoc 주석을 사용해 가독성을 보완하자.
/**
* A service for managing Foo entities.
*/
class Foo {
/**
* @param barService - The service to manage Bar-related operations.
*/
constructor(private readonly barService: BarService) {}
}
생성자에서 초기화하기 보다는 필드 선언 시 초기화하자:
생성자에서 초기화 코드가 필요 없어지는 경우, 생성자를 생략할 수도 있다.
Before
class Foo {
private readonly userList: string[];
constructor() {
this.userList = [];
}
}
After
class Foo {
private readonly userList: string[] = [];
}
인스턴스 생성 후 멤버를 추가하거나 제거하면 VM의 클래스 최적화가 어렵다. 선택적 필드(Optional Field)는 명시적으로
undefined
로 초기화해 최적화에 방해가 되지 않도록 하자.
가시성 규칙
템플릿에 사용되는 속성은 protected
나 public
을 사용하자. private
는 외부 참조를 허용하지 않으므로 템플릿 사용 불가.
가시성 우회 금지:
컴파일러와 최적화 규칙의 안정성을 위해 권장되지 않음.
class Foo {
private bar = 42;
}
const foo = new Foo();
console.log(foo['bar']); // BAD: 가시성 우회
Getter와 Setter란?
Getter: 클래스의 속성을 가져올 때 호출되는 메서드. 속성을 읽는 것처럼 사용할 수 있다.
Setter: 클래스의 속성을 설정할 때 호출되는 메서드.
Getter는 순수 함수여야 한다:
순수 함수(Pure Function)란 입력값에 따라 항상 동일한 결과를 반환하며, 부작용(Side Effects)이 없는 함수다.
Getter는 상태를 읽기만 해야 하며, 상태를 변경하면 안 된다.
Before
class Foo {
nextId = 0;
get next() {
return this.nextId++; // BAD: Getter가 상태를 변경
}
}
After
class Foo {
private nextId = 0;
getNext() {
return this.nextId++; // GOOD: Getter 대신 메서드로 상태 변경
}
}
Getter와 Setter로 구현된 속성은 가시성을 제한할 수 있다:
내부 구현을 숨기거나 간소화된 인터페이스를 제공할 때 유용하다.
숨겨진 속성에는 internal
또는 wrapped
와 같은 접두사나 접미사로 붙일 수 있다.
class Foo {
private wrappedBar = '';
get bar() {
return this.wrappedBar || 'default'; // 내부 변수를 가공해 반환
}
set bar(value: string) {
this.wrappedBar = value.trim(); // 입력값을 가공 후 저장
}
}
비효율적인 Getter/Setter는 피해야 한다:
단순 패스스루(pass-through) 형태의 Getter/Setter는 불필요하다.
이런 경우 속성을 public
으로 선언하거나 readonly
로 지정하는 것이 낫다.
Before
class Bar {
private barInternal = '';
// BAD: Getter/Setter에 추가 로직이 없음.
get bar() {
return this.barInternal;
}
set bar(value: string) {
this.barInternal = value;
}
}
After
class Bar {
public bar: string = ''; // GOOD: 단순한 접근은 public으로 처리
}
Object.defineProperty
로 Getter/Setter 정의 금지:
Object.defineProperty
로 Getter/Setter를 정의하면 속성 이름 변경 시 문제가 발생할 수 있다.
TypeScript의 문법을 활용해 클래스 멤버에 Getter/Setter를 정의해야 한다.
계산된 속성(Computed properties)
계산된 속성은 속성 이름을 동적으로 정의하는 데 사용된다.
예를 들어, 객체 리터럴에서 대괄호 []를 사용해 속성 이름을 계산할 수 있다.
const key = 'dynamicKey';
const obj = {
[key]: 'value', // 계산된 속성
};
console.log(obj.dynamicKey); // 'value'
클래스에서는 계산된 속성 이름으로 문자열이나 숫자 대신 Symbol
만 사용할 것을 권장한다:
Symbol
은 충돌 가능성이 낮아 고유한 속성 이름으로 사용하기 적합하다.const mySymbol = Symbol('myProperty');
class Example {
[mySymbol] = 'This is a Symbol-based property';
getSymbolProperty() {
return this[mySymbol];
}
}
const instance = new Example();
console.log(instance.getSymbolProperty());
Dict-style 속성 금지:
문자열 기반 계산된 속성은 객체의 키 타입을 혼합시켜 코드 일관성을 해치고 유지보수를 어렵게 만든다.
class BadExample {
["dynamicKey"] = 'value';
}
클래스가 논리적으로 반복(iterable) 가능하다면 반드시 [Symbol.iterator]
를 정의해야 한다:
[Symbol.iterator]
메서드는 객체를 이터러블(iterable) 만들어 for...of
루프와 같은 반복문에서 사용할 수 있게 한다.
Symbol
의 사용은 최소화하자. 일부 내장Symbol
(예:Symbol.isConcatSpreadable)
은 TypeScript 컴파일러에서 폴리필되지 않으므로 사용에 주의하자.
가시성을 가능한 한 제한적으로 설정하여, 외부에서 접근할 수 있는 범위를 최소화하자.
코드의 결합도를 낮추고 불필요한 의존성을 방지할 수 있다.
TypeScript는 다음 세 가지 접근 제어자를 제공한다:
public
(기본값): 어디서든 접근 가능.protected
: 클래스 및 서브클래스에서만 접근 가능.private
: 같은 클래스 내부에서만 접근 가능.TypeScript에서는 public
은 기본값이므로, 명시적으로 선언할 필요가 없다:
readonly
속성은 기본적으로 public
이므로, 별도로 public
을 추가할 필요가 없다:
Before
class Foo {
public bar = new Bar(); // BAD: public 선언 불필요
constructor(public readonly baz: Baz) {} // BAD: readonly 자체가 기본적으로 public
}
After
class Foo {
bar = new Bar(); // GOOD: public 생략 가능
constructor(public baz: Baz) {} // GOOD: public은 parameter property에서만 허용
}
private
메서드를 클래스 외부에서 사용할 필요가 없다면, 클래스 외부의 비-내보내기(non-exported) 함수로 변환할 것을 권장한다:
클래스 정의가 단순해지고 외부 접근 불가를 더 명확히 보장하여 코드를 더욱 깔끔하고 안정적으로 유지할 수 있다.
프로토타입 직접 조작 금지:
class
키워드 사용은 클래스 정의를 더 명확하고 읽기 쉽게 만들어줌.Before
// BAD: 기존 객체의 프로토타입을 수정
Array.prototype.customMethod = function () {
return this.length;
};
Array
객체에 customMethod
가 추가됨.After
class CustomArray extends Array {
customMethod(): number {
return this.length;
}
}
const myArray = new CustomArray();
console.log(myArray.customMethod());
프레임워크 코드는 예외적으로 프로토타입 조작을 허용:
Polymer나 Angular와 같은 프레임워크는 성능 최적화, 동작 확장을 위해 프로토타입을 조작해야 할 때가 있음.
이러한 경우는 성능과 구조적 요구를 기반으로 결정됨.
GitHub - google/gts: ☂️ TypeScript style guide, formatter, and linter.
Typescript Google Code Style Part 1
Typescript Google Code Style Part 2
Typescript Google Code Style Part 3