[이펙티브 타입스크립트] 2장 후반부 타입스크립트의 타입 시스템

JIY00N·2023년 8월 1일
3

TypeScript

목록 보기
3/9
post-thumbnail
post-custom-banner

2023.08.01~04 아이템12~18

아이템 12 함수 표현식에 타입 적용하기


  • 자바스크립트와 타입스크립트에서의 함수를 정의하는 방법에는 함수 선언문과 함수 표현식이 있습니다.
// 자바스크립트에서의
// 함수 선언문
function JsStatement(){}

// 함수 표현식
const JsExpression1 = function (){};
const JsExpression2 = () => {};

// 타입스크립트에서의
// 함수 선언문
function TsStatement(a: number): number{}

// 함수 표현식
const TsExpression1 = function (a: number): number{};
const TsExpression2 = (a: number): number => {};
  • 타입스크립트에서는 타입 재사용이라는 관점에서 함수 선언문보다 함수 표현식이 장점을 가진다. 다만, 둘의 차이를 이해하자(함수 호이스팅)

  • 함수 선언문은 호이스팅에 영향을 받지만, 함수 표현식은 호이스팅에 영향을 받지 않는다. 함수 선언문은 코드를 구현한 위치와 관계없이 자바스크립트의 특징인 호이스팅에 따라 브라우저가 자바스크립트를 해석할 때 맨 위로 끌어 올려진다.
    함수 선언문 vs 함수 선언식 개념 및 예제

  • 타입스크립트에서 함수 표현식을 사용하자

1. 타입 재사용: 함수의 매개변수 ~ 반환값 전체를 함수 타입으로 선언할 수 있다.

// 매개변수 interface
interface Params {
  p: string;
  a: boolean;
  r: number;
  m: symbol;
}
// return interface
interface Return {
  r: string;
  e: boolean;
  t: number;
}

// 함수 선언문 -> 함수의 매개변수와 반환값의 타입을 따로 선언
function statement(params: Params): Return{
  return {r: 'r', e: true, t:0}
}

// 함수 표현식 재사용(ExpressionFunction으로 재사용)
type ExpressionFunction = (params: Params) => Return;

// 함수 표현식
const expression1: ExpressionFunction = function (params) {
  return { r: 'r', e: true, t: 0 };
};
const expression2: ExpressionFunction = (params) => ({
  r: 'r',
  e: true,
  t: 0,
});

2. 반복되는 함수 시그니처를 하나로 통합
라이브러리는 공통 함수 시그니처를 타입으로 제공한다.
만약, 라이브러리를 직접 만든다면 공동 콜백에 타입을 제공하자

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;

3. 다른 함수의 시그니처를 참조하려면 typeof fn을 사용하면 된다.

// 함수 전체에 타입(typeof fetch)를 적용 
// -> input과 init 타입 추론 가능, 반환 타입 보장
const simpleCheckedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    throw new Error('Request failed: ' + response.status);
  }
  return response;
};

🎯 요약

타입스크립트에서 함수 선언문보다 함수 표현식을 사용하자

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


  • 타입스크립트에서 타입을 정의하는 방법 두 가지
    1. type
    2. interface

  • type과 interface의 공통점과 차이점

1. 공통점

  1. 타입의 기본 동작
// type과 interface로 타입 선언 가능
type Type = {
  name: string;
};
const type_common: Type = {
  name: 'lee',
};

interface Interface {
  name: string;
}
const interface_common: Interface = {
  name: 'kim',
};
  1. 인덱스 시그니처
type TypeIndexSignature = {
  [key: string]: string;
};
interface InterfaceIndexSignature {
  [key: string]: string;
}
  1. 함수 타입 정의
// 함수 타입 정의
type TypeFunction = {
  (x: number): number;
};
const typeFunction: TypeFunction = (x) => 0;
// 타입 별칭(type alias)로 함수 타입 선언
type TypeAliasFunction = (x: number) => number;
const typeAliasFunction: TypeAliasFunction = (x) => 0;

interface InterfaceFunction {
  (x: number): number;
}
const interfaceFunction: InterfaceFunction = (x) => 0;
  1. 제너릭 가능
// 제너릭 가능
type TypeGeneric<T> = {
  first: T;
};
interface InterfaceGeneric<T> {
  first: T;
}
  1. 타입 확장 가능
    단, 인터페이스는 유니온 타입과 같은 복잡한 타입은 확장 못함
    -> 복잡한 타입을 확장하고 싶으면 type과 & 사용
// 타입 확장 가능
type Type = {
  name: string;
};
interface Interface {
  name: string;
}
type TypeExtended = Interface & { age: number };
interface InterfaceExtended extends Type {
  age: number;
}
  1. 클래스 구현(implements) 가능
// 클래스 구현(implements)
type Type = {
  name: string;
  human: boolean;
};
interface Interface {
  name: string;
}
class TypeClass implements Type {
  name: string = '';
  human: boolean = true;
  age: number = 0;
}
class InterfaceClass implements Interface {
  name: string = '';
  age: number = 12;
}

extends와 implements 차이점
extends - 상속받고자 하는 부모 클래스를 명시
implements - 미리 추상화 된 인터페이스를 채택하여 사용

extends는 이미 구현된 메서드와 프로퍼티를 편하게 사용할 때 쓰면 유용할 거 같고, implements어떠한 조건을 강제할 때 사용하면 유용할 것 같다.

2. 차이점

1. 유니온 개념의 유무
type에는 유니온 타입이 있지만, interface에는 유니온 인터페이스가 없다.

type TypeAorB = 'a' | 'b'
interface InterfaceAorB{
  // ... ?
}

2. type 키워드로 튜플과 배열 타입을 간결하게 표현
type은 메서드 사용 가능, interface는 불가능

3. interface만의 선언 병합 기능
선언 병합 - 컴파일러가 같은 이름으로 선언된 두 개의 개별적인 선언을 하나의 정의로 병합하는 것을 의미한다. 이 병합된 정의는 원래 두 선언 각각의 기능을 모두 가지게 된다. 병합할 선언이 몇 개든 상관 없다. 선언 병합

// 예제1
interface Box{
  height: number;
  width: number;
}
interface Box{
  scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};

// 예제 2
// Window 인터페이스 선언 병합을 통해 속성을 추가
declare global{
  interface Window{
    somethingToWindow: string;
  }
}
window.somethingToWindow; 
// window객체에 Window의 somethingToWindow 선언 병합
  • type VS interface 어느 것을 사용해야 할까?

1. 복잡한 타입 -> type
2. 간단한 객체 타입 -> 일관성과 보강의 관점에서 둘 다 고려
3. api에 대한 타입 선언 -> interface

🎯 요약

타입스크립트의 typeinterface의 차이점과 공통점을 잘 이해해서 적절하게 사용하자

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


  • 타입스크립트에서 타입에서도 DRY(Don't Repeat Yourself)원칙을 적용하자

  • 타입을 재활용 하는 방법

1. 타입 선언을 통해 중복 제거

// 타입 선언을 통해 중복된 타입 제거
// 전
function distance(a: { x: number; y: number }, b: { x: number; y: number }) {}

// 후
type Point2D = {
  x: number;
  y: number;
}
function distance2(a: Point2D, b: Point2D){

}

2. extends로 타입을 확장하여 중복을 제거

// 타입을 확장
// 전
type Point2D = {
  x: number;
  y: number;
};
type Point3D = {
  x: number;
  y: number;
  z: number;
};

// 후
type TPoint3D = Point2D & { z: number };
interface IPoint3D extends Point2D {
  z: number;
}

3. 인덱싱으로 타입을 축소하고, 매핑된 타입으로 정리, Pick 사용

// 전
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}
interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
}
// 후
// 인덱싱으로 타입 축소
type TopNavState = {
  userId: State['userId'];
  pageTitle: State['pageTitle'];
  recentFiles: State['recentFiles'];
}

// 매핑된 타입 사용
type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k];
};

// 완전하지 않은 Pick
type Pick<T,K> = { 
  [k in K]: T[k]
}; // 'K' 타입은 'string | number | symbol' 타입에 할당 할 수 없다.

// pg83 k는 T 타입과 무관하고 범위가 너무 넓다.
// K는 인덱스로 사용될 수 있는 'string | number | symbol'이 되어야 한다.
// K는 실제로 T의 키의 부분 집합, 즉 keyof T가 되어야 한다.

// Pick 사용
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;


// extends를 사용해 매개변수를 제한
// T는 State, K는 TopNavState
// Pick -> K는 T의 키의 부분 집합(=keyof T)
type Pick<T,K extends keyof T> = { [k in K]: T[k]};
/*1. keyof는 T 타입을 받아서 속성 타입의 유니온을 반환 -> ('userId' | 'pageTitle' | 'recentFiles' | 'pageContents')
 2. 아이템 7에서, 타입을 extends 하는 것은 해당 타입의 부분 집합을 의미
 3. 이 부분 집합을 K로 넘겨줌
 4. 매핑된 타입(in keyof Type)을 이용하여 T 타입의 키로 접근하여 타입의 속성에 대한 타입을 가 져옴
*/
  • 유니온의 인덱싱과 Pick의 차이
interface SaveAction {
	type: 'save'
}

interface LoadAction {
	type: 'load'
}

type Action = SaveAction | LoadAction
type ActionType = 'save' | 'load' // 타입의 반복

type ActionType = Action['type'] // 타입은 'save' | 'load'
type ActionRec = Pick<Action, 'type'> // {type: 'save' | 'load'}

4. Partial을 이용하여 기존 타입들의 속성을 모두 Optional로 변경하기

type Something = {
  a: number;
  b: string;
  c: boolean;
};

// 전
type OptionalSomething = {
  a?: number;
  b?: string;
  c?: boolean;
};

// 후
type Partial<T> = { [k in keyof T]?: T[k] };
type OptionalSomething = { [k in keyof Something]?: Something[k] };
/*1. keyof는 타입을 받아서 속성 타입의 유니온을 반환 -> ('a' | 'b' | 'c')
  2. 매핑된 타입(in keyof Type)을 이용하여 Something 내 k 값에 해당하는 속성이 있는지 찾음
  3. ?로 각 속성을 선택적으로 만든다.
*/

5. typeof를 이용하여 객체의 타입을 추출하기
물론, 타입을 먼저 정의하는 것이 좋음

// 전
const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: '#fff',
  label: 'aaa',
};
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}

6. ReturnType으로 함수나 메서드의 반환 값에 명명된 타입을 만들 수 있다.

function getUserInfo(userId: string){
  return{
    userId,
    name,
    age,
};
type UserInfo = ReturnType<typeof getUserInfo>;

🎯 요약

타입스크립트의 타입에서도 타입 연산과 제너릭을 사용해 DRY 원칙을 적용하자

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


  • 인덱스 시그니처
type IndexSignatureType = { 
  [property: string]: string
};
// [키의 이름: 키의 타입]: 값의 타입
  • 인덱스 시그니처의 단점
  1. 모든 키를 허용하기 때문에, 객체에 없는 키를 이용해도 타입 체크에서 에러가 나지 않는다.
  2. 특정 키가 필요하지 않는다. {}도 할당 가능
  3. 키마다 다른 타입을 가질 수 없다.
  4. 타입스크립트의 언어 서비스를 제공받지 못한다.
  • 동적 데이터(계산되고 가공되는 값)가 아니라면 되도록 인덱스 시그니처 사용을 피하고 다른 방법을 찾아야 한다.

  • 타입의 keys들이 무엇이 될지 아는 경우는 인덱스 시그니처보다 타입을 직접 선언해 주는 것이 좋은데, 이에 편의성을 제공해주는 방법들이 있다.

1. Record 사용
Record는 키 타입에 유연성을 제공하는 제너릭 타입이다.
특히, string의 부분 집합을 사용할 수 있다.

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

2. 매핑된 타입 사용

type Vec3D = { [k in 'x' | 'y' | 'z']: number };
// 조건부 타입을 이용하여 특정 조건에서는 다른 타입 만들기
type Vec3D = { [k in 'x' | 'y' | 'z']: k extends 'x' ? string : number };

// 단, 인터페이스에서는 매핑된 타입 사용 불가
interface IVec3D {
  [k in 'x' | 'y' |'z']: number
}
// 오류 발생: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
  • 동적 데이터라면 인덱스 시그니처 사용 고려
    -> keys가 변동될 수 있기 때문에 값의 타입에 null 혹은 undefined를 유니온 타입으로 넣어주는 것을 고려해야 합니다.
    왜냐면, 특정 키가 언제든지 사라질 수 있기 때문이다.
type ExternalType = {
  [key: string]: string | undefined;
}

🎯 요약

동적 데이터가 아니라면 인덱스 시그니처를 사용하지 말자

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


  • 자바스크립트의 배열은 Object 타입이다.

  • 자바스크립트에서 Object는 키와 쌍으로 구성되어 있는데, 키의 타입은 string or symbol만 가능

  • 자바스크립트 엔진에서 자동으로 형변환(number->string)이 되기 때문에, number 타입의 키로도 접근이 가능했던 것

  • 타입스크립트에서는 일관성을 위해 number 타입의 키를 허용

// arr[0]은 내부적으로 arr['0']으로 바뀜
const arr = [1,2,3];
console.log(Object.keys(arr)); // 배열의 key들 ['0','1','2']
  • lib.es5.d.ts에 선언된 Array 인터페이스
    -> number 타입만 키로 허용하고 있지만, 런타임에서는 string 타입으로 형변환
interface Array<T> {
    length: number;
    toString(): string;
    toLocaleString(): string;
    pop(): T | undefined;
    push(...items: T[]): number;
   	// ... 생략
  	// number 타입만 키로 허용
    [n: number]: T;
}
  • ArrayLike (Array 메서드들이 필요 없을 때 사용)

🎯 요약

1. 인덱스 시그니처가 필요한지 고려하고,
2. number 타입의 인덱스 시그니처가 필요하다면 타입을 만들지 말고,
Array, 튜플, ArrayLike를 사용하자

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


  • 타입스크립트의 readonly변경 불가능을 위한 목적으로 사용된다.

  • readonly의 두 가지 다른 쓰임새
    1. 객체 타입 property 앞에 붙는 readonly
    2. Array, 튜플 타입 앞에 붙는 readonly

1. 객체 타입 property 앞에 붙는 readonly

  • 자바스크립트에서의 const
  1. 원시 타입 변수에게는 재할당 금지
  2. 객체에서는 const로 선언된 객체의 속성을 바꿀 수 있다.
    -> 속성 변경을 막으려면 Object.freeze 사용
const object = {
  property: 'good'
}
object.property = 'bad';
console.log(object.property); // bad

-> 즉, 재할당 가능과 객체의 속성 변경 가능은 서로 독립적인 내용

  • 타입스크립트에서의 readonly

1. 객체의 속성 변경을 막을 수 있다.

//readonly로 객체 속성의 변경 막기
type ReadonlyType = {
  readonly prop: string;
}
const test: ReadonlyType = {
  prop: 'a'
}
test.prop ='b';
// Cannot assign to 'prop' because it is a read-only property

2. readonly가 얕게(shallow) 동작한다.

//readonly shallow
type InnerType = {
  innerProp: string;
}
type ReadonlyType = {
  readonly prop: InnerType;
}
const test: ReadonlyType = {
  prop: {
    innerProp: 'inner'
  }
}
test.prop.innerProp = 'change'; // 통과
test.prop = {
  innerProp: 'error'
}
// Cannot assign to 'prop' because it is a read-only property.
  1. 유틸리티 타입 Readonly는 기존 타입의 속성들 모두 readonly로 변경한다.
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

2. Array, 튜플 타입 앞에 붙는 readonly

  1. readonly가 있으면, 배열의 요소를 읽을 수 있으나 새롭게 추가하거나 변경할 수 없다.
  1. 배열을 변경하는 pop, push와 같은 함수들을 사용할 수 없다.
  1. length 속성을 읽을 수 있으나 변경은 불가능하다.

❓ pg95 -> number[] 타입의 기능이 더 많으니까 상위 집합 아닌가?

  • readonly number[] 타입보다는 number[] 타입의 기능이 더 많다.
    즉,readonly number[] 타입은 number[] 타입의 상위 집합이다.
    따라서, number[](변경 가능한 배열) 타입은 readonly number[](readonly 배열) 타입에 할당 가능
    역은 불가능

🎯 요약

readonly를 사용하여 변경하면서 발생하는 오류를 방지하자

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


  • 매핑된 타입을 사용해 관련 값과 타입을 동기화한다.
interface ScatterProps{
  // The data
  xs: number[];
  ys: number[];

  // display
  xRange: [number, number];
  yRange: [number, number];
  color: string;

  // events
  onClick:(x: number, y: number, index: number) => void;
}

const REQUIRES_UPDATE:{[k in keyof ScatterProps]: boolean} = {
  xs: true,
  ys: true,
  xRange: true,
  yRange: true,
  color: true,
  onClick: false,
}

// good1
// 보수적 접근법(실패에 닫힌 접근법)
// 새로운 속성이 추가되면 shouldUpdate 함수는 값이 변경될 때마다 차트를 다시 그림
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
  let k: keyof Scatterprops;
  for(k in oldProps){
    if(oldProps[k] !== newProps[k]){
      if(k !== 'onClick')
        return true;
    }
  }
  return false;
}

// good2
// 실패에 열린 접근법
// 차트의 불필요하게 그려지는 단점은 해결되지만, 누락될 수 있다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
  return(
  	oldProps.xs !== newProps.xs ||
    oldProps.ys !== newProps.ys ||
    oldProps.xRange !== newProps.xRange ||
    oldProps.yRange !== newProps.yRange ||
    oldProps.color !== newProps.color ||
  );
}


// Best
// 매핑된 타입과 객체를 사용하여 타입체커가 동작하도록 한다.
// [k in keyof ScatterProps]는 타입 체커에게 REQUIRES_UPDATE가 
// ScatterProps와 동일한 속성을 가져야 한다는 정보를 제공한다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
  let k: keyof ScatterProps;
  for(k in oldProps){
    if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]){
      return true;
    }
  }
  return false;
}

🎯 요약

매핑된 타입을 사용하여 값을 동기화하자


☑️ 참고 자료
함수 선언식, 함수 표현식
extends와 implements
동적 데이터
Array 인터페이스
선언 병합

profile
블로그 이전 했습니다. https://yoon-log.vercel.app/
post-custom-banner

0개의 댓글