InfoGrab DocsInfoGrab Docs

TypeScript

요약

TypeScript는 GitLab에서 수년 동안 검토되고, 논의되고, 추진되고, 또 거부되어 왔습니다. 메인 프로젝트 외에, TypeScript는 몇몇 위성 프로젝트에서 유용하게 활용되고 있습니다. 다음 GitLab 프로젝트가 TypeScript를 사용합니다.

GitLab 에서의 TypeScript 도입 경과#

TypeScript는 GitLab에서 수년 동안 검토되고, 논의되고, 추진되고, 또 거부되어 왔습니다. 일반적인 결론은 비용이 이점보다 크기 때문에 메인 프로젝트에 TypeScript를 통합할 수 없다는 것입니다.

  • 메인 프로젝트에는 타입이 엄격하게 지정되지 않은 기존 코드가 많습니다.
  • 메인 프로젝트의 주요 기여자가 모두 TypeScript에 익숙하지는 않습니다.

메인 프로젝트 외에, TypeScript는 몇몇 위성 프로젝트에서 유용하게 활용되고 있습니다.

TypeScript를 사용하는 프로젝트#

다음 GitLab 프로젝트가 TypeScript를 사용합니다.

권장 사항#

ESLint 및 TypeScript 구성 설정#

새 TypeScript 프로젝트를 설정할 때는 ESLint와 TypeScript에 엄격한 타입 안전성 규칙을 구성합니다. 이렇게 하면 프로젝트가 가능한 한 타입 안전한 상태로 유지됩니다.

GitLab for VS Code extension 프로젝트는 TypeScript 프로젝트의 보일러플레이트와 구성을 참고하기에 좋은 모델입니다. 해당 프로젝트의 tsconfig.json과 .eslintrc.json을 복사해 사용하는 방안을 고려합니다.

tsconfig.json의 경우는 다음과 같습니다.

  • "strict": true를 사용합니다. 프로젝트에서 가장 강력한 타입 검사 기능을 강제하고 타입 안전성을 재정의하지 못하도록 막습니다.
  • "skipLibCheck": true를 사용합니다. node_modules의 모든 .d.ts 파일이 아니라 참조된 .d.ts 파일만 검사하므로 컴파일 시간이 줄어듭니다.

.eslintrc.json(또는 .eslintrc.js)의 경우는 다음과 같습니다.

any 사용 회피#

any는 어떤 경우에도 피합니다. 프로젝트의 린터에 이미 구성되어 있어야 하지만, 여기에서 다시 한번 짚어 둘 만한 사항입니다.

개발자는 HTTP 응답을 처리하거나 타입이 없는 라이브러리와 상호작용하는 것처럼 도메인 경계를 넘나드는 데이터 구조를 다룰 때 흔히 any에 의존합니다. 처음에는 편리해 보입니다. 그러나 잘 정의된 타입을 선택하거나 unknown과 프레디케이트를 통한 타입 좁히기를 사용하면 이점이 큽니다.

// Bad :(
function handleMessage(data: any) {
  console.log("We don't know what data is. This could blow up!", data.special.stuff);
}

// Good :)
function handleMessage(data: unknown) {
  console.log("Sometimes it's okay that it remains unknown.", JSON.stringify(data));
}

// Also good :)
function isFooMessage(data: unknown): data is { foo: string } {
  return typeof data === 'object' && data && 'foo' in data;
}

function handleMessage(data: unknown) {
  if (isFooMessage(data)) {
    console.log("We know it's a foo now. This is safe!", data.foo);
  }
}

<> 또는 as를 사용한 캐스팅 회피#

<> 또는 as를 사용한 캐스팅은 최대한 피합니다.

타입 캐스팅은 타입 안전성을 명시적으로 우회합니다. 타입 프레디케이트를 사용하는 방안을 고려합니다.

// Bad :(
function handler(data: unknown) {
  console.log((data as StuffContainer).stuff);
}

// Good :)
function hasStuff(data: unknown): data is StuffContainer {
  if (data && typeof data === 'object') {
    return 'stuff' in data;
  }

  return false;
}

function handler(data: unknown) {
  if (hasStuff(data)) {
    // No casting needed :)
    console.log(data.stuff);
  }
  throw new Error('Expected data to have stuff. Catastrophic consequences might follow...');
}

캐스팅이 허용될 만한 드문 경우도 있습니다 (이 테스트 유틸리티를 참고합니다). 그러나 99% 의 경우에는 더 나은 방법이 있습니다.

새 구조체에는 type보다 interface 우선 사용#

새 구조체를 정의할 때는 새 type 별칭을 선언하기보다 새 interface를 선언하는 편을 우선합니다.

인터페이스와 타입 별칭은 겹치는 부분이 많지만, implements 키워드와 함께 쓸 수 있는 것은 인터페이스뿐입니다. 클래스는 type을 implement 할 수 없고 interface만 가능하므로, type을 쓰면 구조체의 활용 범위가 제한됩니다.

// Bad :(
type Fooer = {
  foo: () => string;
}

// Good :)
interface Fooer {
  foo: () => string;
}

TypeScript 가이드의 설명은 다음과 같습니다.

판단 기준이 필요하다면, type의 기능이 필요해지기 전까지는 interface를 사용합니다.

기존 타입의 별칭 정의에 type 사용#

기존 타입, 클래스, 인터페이스의 별칭을 정의할 때는 type을 사용합니다. 변환이 필요하면 TypeScript 유틸리티 타입을 사용합니다.

interface Config = {
  foo: string;

  isBad: boolean;
}

// Bad :(
type PartialConfig = {
  foo?: string;

  isBad?: boolean;
}

// Good :)
type PartialConfig = Partial<Config>;

유니온 타입으로 추론 개선#

// Bad :(
interface Foo { type: string }
interface FooBar extends Foo { bar: string }
interface FooZed extends Foo { zed: string }

const doThing = (foo: Foo) => {
  if (foo.type === 'bar') {
    // Casting bad :(
    console.log((foo as FooBar).bar);
  }
}

// Good :)
interface FooBar { type: 'bar', bar: string }
interface FooZed { type: 'zed', zed: string }
type Foo = FooBar | FooZed;

const doThing = (foo: Foo) => {
  if (foo.type === 'bar') {
    // No casting needed :) - TS knows we are FooBar now
    console.log(foo.bar);
  }
}

향후 계획#

  • TypeScript 프로젝트 전반에서 재사용할 공유 ESLint 구성.

관련 주제#

TypeScript

GitLab v19.4
원문 보기

요약

TypeScript는 GitLab에서 수년 동안 검토되고, 논의되고, 추진되고, 또 거부되어 왔습니다. 메인 프로젝트 외에, TypeScript는 몇몇 위성 프로젝트에서 유용하게 활용되고 있습니다. 다음 GitLab 프로젝트가 TypeScript를 사용합니다.

GitLab 에서의 TypeScript 도입 경과#

TypeScript는 GitLab에서 수년 동안 검토되고, 논의되고, 추진되고, 또 거부되어 왔습니다. 일반적인 결론은 비용이 이점보다 크기 때문에 메인 프로젝트에 TypeScript를 통합할 수 없다는 것입니다.

  • 메인 프로젝트에는 타입이 엄격하게 지정되지 않은 기존 코드가 많습니다.
  • 메인 프로젝트의 주요 기여자가 모두 TypeScript에 익숙하지는 않습니다.

메인 프로젝트 외에, TypeScript는 몇몇 위성 프로젝트에서 유용하게 활용되고 있습니다.

TypeScript를 사용하는 프로젝트#

다음 GitLab 프로젝트가 TypeScript를 사용합니다.

권장 사항#

ESLint 및 TypeScript 구성 설정#

새 TypeScript 프로젝트를 설정할 때는 ESLint와 TypeScript에 엄격한 타입 안전성 규칙을 구성합니다. 이렇게 하면 프로젝트가 가능한 한 타입 안전한 상태로 유지됩니다.

GitLab for VS Code extension 프로젝트는 TypeScript 프로젝트의 보일러플레이트와 구성을 참고하기에 좋은 모델입니다. 해당 프로젝트의 tsconfig.json과 .eslintrc.json을 복사해 사용하는 방안을 고려합니다.

tsconfig.json의 경우는 다음과 같습니다.

  • "strict": true를 사용합니다. 프로젝트에서 가장 강력한 타입 검사 기능을 강제하고 타입 안전성을 재정의하지 못하도록 막습니다.
  • "skipLibCheck": true를 사용합니다. node_modules의 모든 .d.ts 파일이 아니라 참조된 .d.ts 파일만 검사하므로 컴파일 시간이 줄어듭니다.

.eslintrc.json(또는 .eslintrc.js)의 경우는 다음과 같습니다.

any 사용 회피#

any는 어떤 경우에도 피합니다. 프로젝트의 린터에 이미 구성되어 있어야 하지만, 여기에서 다시 한번 짚어 둘 만한 사항입니다.

개발자는 HTTP 응답을 처리하거나 타입이 없는 라이브러리와 상호작용하는 것처럼 도메인 경계를 넘나드는 데이터 구조를 다룰 때 흔히 any에 의존합니다. 처음에는 편리해 보입니다. 그러나 잘 정의된 타입을 선택하거나 unknown과 프레디케이트를 통한 타입 좁히기를 사용하면 이점이 큽니다.

// Bad :(
function handleMessage(data: any) {
  console.log("We don't know what data is. This could blow up!", data.special.stuff);
}

// Good :)
function handleMessage(data: unknown) {
  console.log("Sometimes it's okay that it remains unknown.", JSON.stringify(data));
}

// Also good :)
function isFooMessage(data: unknown): data is { foo: string } {
  return typeof data === 'object' && data && 'foo' in data;
}

function handleMessage(data: unknown) {
  if (isFooMessage(data)) {
    console.log("We know it's a foo now. This is safe!", data.foo);
  }
}

<> 또는 as를 사용한 캐스팅 회피#

<> 또는 as를 사용한 캐스팅은 최대한 피합니다.

타입 캐스팅은 타입 안전성을 명시적으로 우회합니다. 타입 프레디케이트를 사용하는 방안을 고려합니다.

// Bad :(
function handler(data: unknown) {
  console.log((data as StuffContainer).stuff);
}

// Good :)
function hasStuff(data: unknown): data is StuffContainer {
  if (data && typeof data === 'object') {
    return 'stuff' in data;
  }

  return false;
}

function handler(data: unknown) {
  if (hasStuff(data)) {
    // No casting needed :)
    console.log(data.stuff);
  }
  throw new Error('Expected data to have stuff. Catastrophic consequences might follow...');
}

캐스팅이 허용될 만한 드문 경우도 있습니다 (이 테스트 유틸리티를 참고합니다). 그러나 99% 의 경우에는 더 나은 방법이 있습니다.

새 구조체에는 type보다 interface 우선 사용#

새 구조체를 정의할 때는 새 type 별칭을 선언하기보다 새 interface를 선언하는 편을 우선합니다.

인터페이스와 타입 별칭은 겹치는 부분이 많지만, implements 키워드와 함께 쓸 수 있는 것은 인터페이스뿐입니다. 클래스는 type을 implement 할 수 없고 interface만 가능하므로, type을 쓰면 구조체의 활용 범위가 제한됩니다.

// Bad :(
type Fooer = {
  foo: () => string;
}

// Good :)
interface Fooer {
  foo: () => string;
}

TypeScript 가이드의 설명은 다음과 같습니다.

판단 기준이 필요하다면, type의 기능이 필요해지기 전까지는 interface를 사용합니다.

기존 타입의 별칭 정의에 type 사용#

기존 타입, 클래스, 인터페이스의 별칭을 정의할 때는 type을 사용합니다. 변환이 필요하면 TypeScript 유틸리티 타입을 사용합니다.

interface Config = {
  foo: string;

  isBad: boolean;
}

// Bad :(
type PartialConfig = {
  foo?: string;

  isBad?: boolean;
}

// Good :)
type PartialConfig = Partial<Config>;

유니온 타입으로 추론 개선#

// Bad :(
interface Foo { type: string }
interface FooBar extends Foo { bar: string }
interface FooZed extends Foo { zed: string }

const doThing = (foo: Foo) => {
  if (foo.type === 'bar') {
    // Casting bad :(
    console.log((foo as FooBar).bar);
  }
}

// Good :)
interface FooBar { type: 'bar', bar: string }
interface FooZed { type: 'zed', zed: string }
type Foo = FooBar | FooZed;

const doThing = (foo: Foo) => {
  if (foo.type === 'bar') {
    // No casting needed :) - TS knows we are FooBar now
    console.log(foo.bar);
  }
}

향후 계획#

  • TypeScript 프로젝트 전반에서 재사용할 공유 ESLint 구성.

관련 주제#