[TypeScript 독학] #7 이펙티브 타입스크립트(1)

안광의·2022년 3월 30일
0

TypeScript 독학

목록 보기
7/12
post-thumbnail

시작하며

프론트엔드 개발자로 입사를 하고 가장 먼저 한 일은 타입스크립트 공부였다. 회사에서 진행 중인 프로젝트는 모두 타입스크립트와 리액트 기반였고, 이전에 타입스크립트에 대해 공부했지만 실제 프로젝트에서 사용하기에는 부족했다. 이펙티브 타입스크립트라는 책을 추천받아 읽게 되었고 관련된 내용을 간단하게 정리해보려고 한다.

이펙티브 타입스크립트

짧은 코드 예제와 함께 타입스크립트 코드를 작성하는 전략들이 설명되어 있는 책으로, 기본적인 타입스크립트 문법에 대해서 이해하고 있는 사람을 대상으로 쓰여진 책이다. 그렇다고 내용이 이해하기 어렵지는 않아서 자바스크립트를 알고 있고 타입스크립트를 공부하려고 하는 사람에게, 특히 실무에 타입스크립트를 활용해야 하는 사람에게 잘 맞는 책이라고 생각한다.



1장 타입스크립트 알아보기

타입스크립트 런타임과 타입체커는 분리되어 있다.

const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());

//타입체크 통과, 런타임시 에러

타입스크립트는 자바스크립트의 확장판이라고 할 수 있고 실제로 자바스크립트로 컴파일되어 실행된다. 코드를 실행하는 부분과 타입체커가 분리되어 있기 때문에 코드를 작성할 때 타입 에러가 발생하더라도 런타임에는 정상 작동한다. 위의 예는 그 반대의 경우로 name의 타입을 string[]으로 추론해서 타입체크는 문제가 없지만 런타임시 에러가 발생한다.


any 타입의 사용을 지양해야 한다.

   let age: number;
   age = '12' as any;  // OK
function calculateAge(birthDate: Date): number {
  return 0;
}

let birthDate: any = '1990-01-19';
calculateAge(birthDate);  // OK

any 타입은 시그니처를 무시해버리고 자동완성 기능도 사용할 수 없기 때문에 사용을 지양해야 한다.



2장 타입스크립트의 타입시스템

타입을 얻을 때는 반드시 bracket notation으로

interface Person {
  first: string;
  last: string;
}
const p: Person = { first: 'Jane', last: 'Jacobs' };
const first: Person['first'] = p['first'];  // Or p.first

타입 단언보다는 타입 선언을 사용해야 한다.

interface Person { name: string };
const alice: Person = {};
   // ~~~~~ Property 'name' is missing in type '{}'
   //       but required in type 'Person'
const bob = {} as Person;  // No error

타입 단언은 강제로 타입을 지정하여 오류가 발생하지 않는다.


접미사 !는 null이 아니라는 단언문

const elNull = document.getElementById('foo');  // Type is HTMLElement | null
const el = document.getElementById('foo')!; // Type is HTMLElement

as unknown as

interface Person { name: string; }
const body = document.body;
const el = body as Person;

body타입은 Person 타입과 충분히 겹치지 않아 단언문을 사용할 수 없고 모든 타입은 unknown 타입의 서브타입이기 때문에 as unknown as Person으로 단언문을 작성할 수 있다.


객체 래퍼말고 기본형을 사용해야한다. 예외) Symbol, BigInt

const s: String = "primitive";
const n: Number = 12;
const b: Boolean = true;
//string, number, boolean 사용
function isGreeting(phrase: String) {
  return [
    'hello',
    'good day'
  ].includes(phrase);
          // ~~~~~~
          // Argument of type 'String' is not assignable to parameter
          // of type 'string'.
          // 'string' is a primitive, but 'String' is a wrapper object;
          // prefer using 'string' when possible
}

stringString에 할당할 수 있지만 Stringstring에 할당이 불가능하다.


잉여 속성 체크

interface Options {
  title: string;
  darkMode?: boolean;
}
const intermediate = { lightMode: true, title: 'Ski Free' };
const o: Options = intermediate;  // OK

객체 리터럴일때만 잉여 속성 체크를 하기 때문에 임시변수를 사용하면 체크를 건너띈다.

interface LineChartOptions {
  logscale?: boolean;
  invertedYAxis?: boolean;
  areaChart?: boolean;
}
const opts = { logSize: true };
const o: LineChartOptions = opts;

선택적 속성만 가지는 weak 타입은 잉여 속성체크가 동작한다.


재사용할 수 있는 표현식을 사용하는 것이 좋다.

type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => { /* COMPRESS */ return 0; /* END */ };

함수의 매개변수부터 반환값까지 전체를 타입으로 선언하여 함수표현식에 재사용할 수 있기 때문에 타입스크립트에서는 표현식이 좋다.

type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;

함수표현식을 사용한 타입 재사용의 예


interface vs type

  • type, interface 기능적으로는 차이가 없다.

  • 인덱스 시그니처는 type, interface 모두 사용 가능하다.

type TState = {
  name: string;
  capital: string;
}
interface IState {
  name: string;
  capital: string;
}
type TDict = { [key: string]: string };
interface IDict {
  [key: string]: string;
}
  • 함수타입도 type, interface 모두 사용 가능하다.

  • type, interface 모두 제너릭 사용 가능하다.

  • type은 extends가 불가능하므로 &를 사용해서 확장해야 한다.

  • 유니온 타입은 존재하나, 유니온 인터페이스는 존재하지 않는다.

  • 튜플과 배열 타입을 정의하는 데 type이 더 간결하다.

type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];
interface Tuple {
  0: number;
  1: number;
  length: 2;
}
const t: Tuple = [10, 20];  // OK
  • interface는 선언 병합이 가능하다.
interface IState {
  name: string;
  capital: string;
}
interface IState {
  population: number;
}
const wyoming: IState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000
}; 

결론

  • 복잡한 타입 => type
  • 간단한 타입 => 일관성의 측면을 고려해서 type or interface
  • 단, 보강의 가능성이 있는 새 프로젝트라면 interface

타입 중복을 고려하여 코드를 작성해야 한다.

interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate extends Person {
  birth: Date;
}

type PersonWithBirthDate = Person & { birth: Date };

extends 키워드와 &를 사용한 타입 중복 제거


interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}
type TopNavState = {
  userId: State['userId'];
  pageTitle: State['pageTitle'];
  recentFiles: State['recentFiles'];
};

TopNavState이 State의 부분집합임으로 TopNavState을 확장하여 중복을 제거하기 보다 인덱싱하여 중복을 제거


interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}
type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};

in 키워드를 사용하여 중복 타입&중복 코드 제거 : Pick키워드와 동일


interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
type OptionsUpdate = {[k in keyof Options]?: Options[k]};

선택적 필드를 가진 타입은 in keyof와 ?를 사용하여 구현 : Partial 키워드와 같음


인덱스 시그니처

type Rocket = {[property: string]: string};
const rocket: Rocket = {
  name: 'Falcon 9',
  variant: 'v1.0',
  thrust: '4,940 kN',
};

인덱스 시그니처의 단점

  • 잘못된 키도 허용함
  • 필수적인 키가 없음
  • 키마다 다른 타입을 가질 수 없음
  • 자동완성 기능 사용 불가

배열 타입은 for of를 사용하는 것이 좋다.

const xs = [1, 2, 3];
for (const x of xs) {
  x;  // Type is number
}
const xs = [1, 2, 3];
for (let i = 0; i < xs.length; i++) {
  const x = xs[i];
  if (x < 0) break;
}

루프 중간에 멈춰야 하는 경우 for문을 사용하고 인덱스 시그니처에 number를 사용하기보다 튜플이나 배열을 사용하는 것이 좋다.

const xs = [1, 2, 3];
function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
  if (i < xs.length) {
    return xs[i];
  }
  throw new Error(`Attempt to access ${i} which is past end of array.`)
}

튜플을 사용하고 싶다면 ArrayLike 사용할 수 있다.



마치며

기본적인 설명부터 깊이있는 내용까지 코드를 작성하는데 있어서 넓은 범위의 내용을 예제 코드와 함께 설명하고 있는 책이여서 도움이 된다고 생각한다. 내용이 많고 실무에 어떻게 활용할지 감이 안잡히는 부분이 있기 때문에 앞으로 계속 찾아보면서 익혀야 하는 책이라고 생각한다.

profile
개발자로 성장하기

0개의 댓글