[이펙티브 타입스크립트] 2장. 타입 시스템

jaejin·2023년 5월 6일

아이템13. 타입과 인터페이스의 차이점 알기

type TState = {
  name: string;
  capital: string;
}
  
interface IState {
   name: string;
   capital: string;
}
  • 기본적인 사용 방법은 유사하다.

유사한 점

인덱스 시그니쳐

type TDict = { [key: string]: string };
interface IDict {
  [key: string]: string;
}

함수 타입

type TFn = (x:number) => string;
interface IFn {
	(x: number): string;
}

제네릭

type TPair<T> = {
	first: T;
	second: T;
}
interface IPair<T> {
	first: T;
	second: T;
}

확장

interface IStateWithPop extends TState {
	population: number;
}
type TStateWithPop = IState & { population: number; }
  • IStateWithPopTStateWithPop은 동일하다.
  • 인터페이스는 유니온 타입 같은 복잡한 타입은 확장하지 못한다.
  • 복잡한 타입 확장에는 타입과 &를 사용해야 한다.

클래스

class StateT implements TState {
	name: string = '';
	capital: string = '';
}
class StateI implements IState {
	name: string = '';
	capital: string = '';
}

차이점

타입의 장점

유니온

type Input = { /* ... */ };
type Output = { /* ... */ };

interface VariableMap {
	[name: string]: Input | Output;
}
type NamedVariable = (Input | Output) & { name: string };
  • 유니온 타입은 있지만, 유니온 인터페이스는 없다.
  • 유니온 타입의 확장은 위와 같은 방법으로 가능하다.

튜플, 배열

type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];
  • 튜플과 배열 타입은 type 키워드를 이용해 간결하게 표현 가능하다.

인터페이스의 장점

보강

interface IState {
	name: string;
 	capital: stirng;
}
interface IState {
	population: number;
}
const wyoming: IState = {
	name: 'Wyoming',
 	capital: 'Cheyenne',
 	population: 500_000,
}; // 정상
  • 위 예제와 같이 속성을 확장하는 것을 선언 병합이라고 한다.

타입 or 인터페이스 ?

  • 복잡한 타입이라면 타입 별칭을 사용하는게 좋다.
  • 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려
  • 어떤 API에 대한 타입 선언을 작성해야 한다면 인터페이스를 사용하는게 좋다.
  • API가 변경될 때 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있어 유용하기 때문에
  • 프로젝트 내부적으로 사용되는 타입은 바뀌어서는 안된다.
  • 이럴 때는 타입을 사용해야 한다.

아이템14. 타입 연산과 제너릭 사용으로 반복 줄이기

반복 줄이기

interface State {
	userId: string;
 	pageTitle: string;
 	recentFiles: string[];
 	pageContents: string;
}
interface TopNavState {
	userId: stirng;
 	pageTitle: string;
 	recentFiles: string[];
}

전체 애플리케이션의 상태를 표현하는 State 타입과 단지 부분만 표현하는 TopNavState가 있는 경우이다.

TopNavState를 확장하여 State를 구성하기보다, State의 부분 집합으로 TopNavState를 정의하는 것이 바람직하다. 이 방법이 전체 앱의 상태를 하나의 인터페이스로 유지할 수 있게 해준다.

type TopNavState = {
	[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
  • 맵핑된 타입을 사용해서 반복을 최소화 할 수 있다.

아이템15. 동적 데이터에 인덱스 시그니처 사용하기

type Rocket - {[property: string]: string};
const rocket: Rocket = {
	name: 'Falcon 9',
  	variant: 'v1.0',
  	thrust: '4,940 kN',
};
  • 타입스크립트에서 타입에 인덱스 시그니처를 명시하면 유연하게 매핑을 표현할 수 있다.
  • 타입이 너무 광범위하기 때문에 런타임 때까지 객체의 속성을 알 수 없을 경우에만 사용하는게 좋다.
  • 안전한 접근을 위해 인덱스 시그니처의 값 타입에 undefined를 추가하는 것을 고려해야 한다.

대안

  1. Record 사용
type Vec3D = Record<'x', 'y', 'z', number>;
// Type Vec3D = {
//	x: number;
//	y: number;
//	z: number;
// }
  1. 매핑된 타입 사용
type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
// Type Vec3D = {
//	x: number;
//	y: string;
//	z: number;
// }

아이템16. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기

배열의 순회

  • 타입이 불확실하다면, for-in 루프는 for-of 또는 C 스타일 for 루프에 비해 몇 배나 느리다.

요약

  • 배열은 객체이므로 키는 숫자가 아니라 문자열이다. 인덱스 시그니처로 사용된 number 타입은 버그를 잡기 위한 순수 타입스크립트 코드이다.
  • 인덱스 시그니처에 number를 사용하기보다 Array나 튜플, 또는 ArrayLike 타입을 사용하는게 좋다.

아이템17. 변경 관련된 오류 방지를 위해 readonly 사용하기

function arraySum(arr: readonly number[]){
	let sum = 0, num;
  	while((num = arr.pop()) !== undefined){
      				// ~~~ 'readonly number[]' 형식에 'pop' 속성이 없습니다.
		sum += num;
  	}
  	return sum;
}
  • 자바스크립트나 타입스크립트에서 명시적으로 언급하지 않는 한, 함수가 매개변수를 변경하지 않는다고 가정한다. 그러나 명시적으로 readonly 접근 제어자를 사용하는것이 컴파일러와 사람 모두에게 좋다.

readonly 단점

  • 함수가 매개변수를 변경하지 않는다면, readonly로 선언해야 한다.
  • 어떤 함수를 readonly로 만들면, 그 함수를 호출하는 다른 함수도 모두 readonly로 만들어야 한다.
  • 인터페이스를 명확히 하고 타입 안정성을 높일 수 있기 때문에 꼭 단점이라고 볼 순 없다.
  • 다른 라이브러리에 있는 함수를 호출하는 경우, 타입 선언을 바꿀 수 없으므로 타입 단언문을 사용해야 한다.

case 1

function parseTaggedText(lines: string[]): string[][] {
  const currPara: readonly string[] = [];
  const paragraphs: string[][] = [];

  const addParagraph = () => {
    if (currPara.length) {
      paragraphs.push(
        currPara
     // ~~~~~~~~ Type 'readonly string[]' is 'readonly' and
     //          cannot be assigned to the mutable type 'string[]'
      );
      currPara.length = 0;  // Clear lines
            // ~~~~~~ Cannot assign to 'length' because it is a read-only 
            // property
    }
  };

  for (const line of lines) {
    if (!line) {
      addParagraph();
    } else {
      currPara.push(line);
            // ~~~~ Property 'push' does not exist on type 'readonly string[]'
    }
  }
  addParagraph();
  return paragraphs;
}
  • readonly로 선언하면 위와 같은 오류가 발생
  • currPara를 let으로 선언하고 변환이 없는 메서드를 사용함으로써 두 개의 오류를 고칠 수 있다.
let currPara: readonly string[] = [];
// ...
currPara = []; // 배열을 비움
// ...
currPara = currPara.concat([line]);
  • push와 달리 concat은 원본을 수정하지 않고 새 배열을 반환한다.
  • let, readonly로 수정함으로써 currPara 변수는 가리키는 배열을 자유롭게 변경할 수 있지만, 그 배열 자체는 변경하지 못한다.
  • 여전히 paragraphs에 대한 오류는 남아있다. 이 오류를 바로잡는 방법은 세 가지가 있다.
  1. currPara의 복사본을 만드는 방법
paragraphs.push([...currPara]);
  • currPara는 readonly로 유지되지만, 복사본은 원하는 대로 변경이 가능하기 때문에 오류가 사라진다.
  1. paragraphs(그리고 함수의 반환 타입)를 readonly string[]의 배열로 변경하는 방법
const paragraphs: (readonly string[])[] = [];
  • 이미 함수가 반환한 값에 대해 영향을 끼치는 것이 맞는 방법인지 고민해야 한다.
  • 1번의 방법은 이미 함수가 반환한 값이 변경이 될 수 있다.
  1. 단언문을 쓰는 방법
paragraphs.push(currPara as string[]);
  • 바로 다음 문장에서 currPara를 새 배열에 할당하므로, 매우 공격적인 단언문은 아니다.

주의할 점

  • readonly는 얕게 동작한다. 객체의 readonly 배열이 있다면, 그 객체 자체는 readonly가 아니다.
  • 제너릭을 만들면 깊은 readonly 타입을 사용할 수 있지만 만들기 까다롭기 때문에 라이브러리를 사용하는게 낫다.
  • ts-essentials에 있는 DeepReadonly 제너릭을 사용하면 된다.

아이템18. 매핑된 타입을 사용하여 값을 동기화하기

interface ViewProps {
  // data
  xs: number[];
  ys: number[];
  // display
  xRange: [number,number];
  yRange: [number, number];
  color: string;
  // event
  onClick: (x:number, y: number, index: number) => void;
}
  • 데이터나 디스플레이가 아닌 이벤트 핸들러가 변경되면 차트를 다시 그릴 필요가 없다.
  • 이를 최적화하기 위해서는 타입체커를 활용할 수 있다.
const REQUIRES_UPDATE: {[k in keyof ViewProps]: boolean} = {
 xs: true,
 ys: true,
 xRange: true,
 yRange: true,
 color: true,
 onClick: false
 }

function shouldUpdate(
 oldProps: ViewProps,
 newProps: ViewProps,
 ) {
   let k: keyof ViewProps;
   for (k in oldProps){
     if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]){
       return true;
     }
   }
 }
  • [k in keyof ScatterProps]은 타입 체커에게 REQUIRES_UPDATE가 ScatterProps과 동일한 속성을 가져야 한다는 정보를 제공한다.
  • 나중에 ScatterProps에 새로운 속성을 추가하는 경우 오류가 발생한다.
profile
jjlabsio

0개의 댓글