TypeScript

GitLab과의 역사

TypeScript는 GitLab에서 수년간 고려되었고, 토론되었고, 홍보되었으며, 거부되었습니다. 일반적인 결론은 비용이 혜택을 상회하여 TypeScript를 기본 프로젝트에 통합할 수 없다는 것입니다.

  • 기본 프로젝트에는 강하게 유형 지정되지 않은 사전 코드가 많습니다.
  • 기본 프로젝트의 주요 기여자들은 TypeScript를 모두 알지 못합니다.

기본 프로젝트를 제외하고 TypeScript는 소수의 위성 프로젝트에서 수익성 있게 활용되었습니다.

TypeScript를 사용하는 프로젝트

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

권장 사항

ESLint 및 TypeScript 구성 설정

새로운 TypeScript 프로젝트를 설정할 때, ESLint 및 TypeScript에 대한 엄격한 유형 안전 규칙을 구성하세요. 이렇게 하면 프로젝트가 가능한 한 유형 안전 상태를 유지할 수 있습니다.

GitLab Workflow 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을 사용하고 예측을 통해 유형을 좁히는 것)을 선택하는 것이 상당한 이점을 가져옵니다.

// 안 좋음 :(
function handleMessage(data: any) {
  console.log("데이터가 무엇인지 모릅니다. 이건 고장일 수 있어요!", data.special.stuff);
}

// 좋음 :)
function handleMessage(data: unknown) {
  console.log("때로는 그것이 알려지지 않아도 상관없습니다.", JSON.stringify(data));
}

// 또 좋음 :)
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("우리는 지금 그것이 foo라는 것을 알고 있습니다. 안전합니다!", data.foo);
  }
}

<>, as로 캐스팅 피하기

가능한 한 <>, as로 캐스팅하는 것을 피하세요.

명시적으로 타입을 캐스팅하는 것은 유형 안전을 우회하는 것입니다. 유형 예측을 사용하는 것을 고려해보세요.

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

// 좋음 :)
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)) {
    // 캐스팅이 필요 없습니다 :)
    console.log(data.stuff);
  }
  throw new Error('데이터가 stuff을 가져야 한다고 예상했습니다. 재앙이 따를 수 있습니다...');
}

이 테스트 유틸리티을 고려할 때 드물게 허용되기도 합니다. 그러나 99%의 경우 더 나은 방법이 있습니다.

새로운 구조체를 정의할 때는 type 대신 interface를 선호하세요

새로운 구조체를 정의할 때 새로운 type alias를 선언하는 대신 새로운 interface를 선언하는 것을 선호하세요.

인터페이스와 타입 alias는 많은 부분에서 겹치지만, implements 키워드와 함께 사용할 수 있는 것은 인터페이스뿐입니다. 클래스는 typeimplements할 수 없기 때문에(interface만 가능), type을 사용하면 구조체의 유용성이 제한됩니다.

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

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

TypeScript 가이드에서:

휴리스틱을 원한다면, type 기능을 사용해야 할 때까지는 interface를 사용하세요.

기존 타입에 별칭 정의에 type을 사용하세요

기존 타입, 클래스 또는 인터페이스에 별칭을 정의하기 위해 type을 사용하세요. TypeScript 유틸리티 타입을 사용하여 변환을 제공하세요.

interface Config = {
  foo: string;

  isBad: boolean;
}

// 나쁨 :(
type PartialConfig = {
  foo?: string;

  isBad?: boolean;
}

// 좋음 :)
type PartialConfig = Partial<Config>;

유니온 타입을 사용하여 추론을 개선하세요

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

const doThing = (foo: Foo) => {
  if (foo.type === 'bar') {
    // 캐스팅 나쁨 :(
    console.log((foo as FooBar).bar);
  }
}

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

const doThing = (foo: Foo) => {
  if (foo.type === 'bar') {
    // 캐스팅 불필요 :) - TS가 우리가 현재 FooBar임을 알고 있습니다
    console.log(foo.bar);
  }
}

향후 계획

  • TypeScript 프로젝트 전체에서 재사용할 수 있는 공통 ESLint 구성.

관련 주제