이펙티브 타입스크립트 item12 ~ 18

이수빈·2025년 5월 23일
0

Typescript

목록 보기
18/20
post-thumbnail

item12.함수 표현식에 타입 적용하기

  • TS에서는 함수 표현식을 사용하는 것이 좋다.

  • 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 => 표현식에 재사용가능하다.

function rollDice1(sides: number): number { /* ... */ }  // Statement
const rollDice2 = function(sides: number): number { /* ... */ };  // Expression
const rollDice3 = (sides: number): number => { /* ... */ };  // Also expression
  • 함수의 매개변수의 타입만 정의하는 것이 아니라 함수 표현식 전체 타입을 정의하는 것이 코드도 간결하고 안전하다.

  • 아래와 같이 매개변수의 type 만 명시해줘도 정상적으로 동작한다. 하지만 return type에 대한 정의가 없기 때문에 throw를 return으로 바꿨을 때 에러를 잡아내지 못한다.

async function checkedFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);
  if (!response.ok) {
    // An exception becomes a rejected Promise in an async function.
    throw new Error(`Request failed: ${response.status}`); // 만약 이 부분이 return이라면?
  }
  return response;
  • 그래서 명시적으로 함수표현식의 타입을 명시적 정의, 재사용하는게 더 좋은 코드이다.
declare function fetch(
  input: RequestInfo, init?: RequestInit,
): Promise<Response>;

const checkedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }
  return response;
}

item13.타입과 인터페이스 차이점

  • 대부분은 비슷한점이 많다.

  • class를 구현할 때 type과 인터페이스 모두 구현이 가능하다.

class StateT implements TState {
  name: string = '';
  capital: string = '';
}
class StateI implements IState {
  name: string = '';
  capital: string = '';
}
  • 차이점

  • 인터페이스는 타입을 확장 할 수는 있지만 유니언을 할 수는 없다. => type써서 유니온 해야한다.

  • 튜플, 배열타입은 type을 쓰는게 더 낫다.

type Pair = [a: number, b: number];
type StringList = string[];
type NamedNums = [string, ...number[]];
  • interface는 선언병합이 가능하다. (사용자가 인터페이스를 통해 새로운 필드를 병합 할 수 있기 때문)
  • 프로젝트 내부적으로 사용되는 타입에 선언병합이 발생하는 것은 잘못된 설계이다.

item14.타입 연산과 제너릭으로 반복 줄이기

  • mapped type 기법 : 배열의 필드를 루프 도는 것과 같은 방식이다.

  • 아래와 같은 타입이 있을 때 => State Type에 매핑된 타입을 사용하면 중복을 제거 할 수 있다.

  • Omit도 사용가능.

interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}
interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  // omits pageContents
}
interface TopNavState {
  userId: State['userId'];
  pageTitle: State['pageTitle'];
  recentFiles: State['recentFiles'];
}; //better

type TopNavState = {
  [K in 'userId' | 'pageTitle' | 'recentFiles']: State[K]
}; // best

type Pick<T,K extends keyof T> = { [k in K] : T[K] };
  • 인덱싱을 잘 확용할 것!, Pick으로 얻게되는 타입과는 다름
interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load';  // Repeated types!

type ActionType = Action['type']; // best, 'save'| 'loading' 
  • Type 프로퍼티 optional로 만들기, 유틸리티 타입도 존재함, Partial
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
interface OptionsUpdate {
  width?: number;
  height?: number;
  color?: string;
  label?: string;
}
class UIWidget {
  constructor(init: Options) { /* ... */ }
  update(options: OptionsUpdate) { /* ... */ }
}

type OptionsUpdate = { [k in keyof Options]? : Options[k] }
  • 함수의 반환값 type을 얻고 싶을때
function getUserInfo(userId: string) {
  // ...
  return {
    userId,
    name,
    age,
    height,
    weight,
    favoriteColor,
  };
}
// Return type inferred as { userId: string; name: string; age: number, ... }

type UserInfo = ReturnType<typeof getUserInfo>;
  • 제너릭 타입은 타입을 위한 함수와 같다.

item15.동적 데이터에 인덱스 시그니처 적용

  • 아래와 같이 인덱스시그니처 타입을 사용하면 동적으로 key value를 추가 할 수 있다.
type Rocket = {[property: string]: string};
const rocket: Rocket = {
  name: 'Falcon 9',
  variant: 'v1.0',
  thrust: '4,940 kN',
};  // OK
  • 하지만 다음과 같은 단점도 존재한다.
  • 잘못된 키를 포함해 모든 키를 허용함. (string 타입이기만 하면됨)
  • 특정키가 필요하지 않음.( {} 도 유효한 Rocket type이다)
  • key마다 다른 타입을 가질 수 없다. 예를들어 thrust는 string이 아니라 number 여야 할 수도 있다.
  • 자동완성 기능이 동작하지 않는다.
  • 이런 단점들이 있기 때문에 타입을 단언 할 수 있을때는 인덱스 시그니처 사용을 지양하자. (동적으로 추가할때만 사용하자)

  • 어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링 하면 안된다.

  • 데이터에 a,b,c,d와 같은 key가 존재하지만 얼마나 많은지 모른다면? => Record를 사용하자. key값을 제한가능하다.

interface Row1 { [column: string]: number }  // Too broad
interface Row2 { a: number; b?: number; c?: number; d?: number }  // Better
type Row3 =
    | { a: number; }
    | { a: number; b: number; }
    | { a: number; b: number; c: number;  }
    | { a: number; b: number; c: number; d: number };  // Also better
    
    type Vec3D = Record<'x' | 'y' | 'z', number>;
//   ^? type Vec3D = {
//        x: number;
//        y: number;
//        z: number;
//      }
  • 매핑된 타입을 사용할 수도 있다. => key마다 별도의 타입을 사용가능하게 해준다.
type ABC = {[k in 'a' | 'b' | 'c'] : k extends 'b'? string : number}

const test:ABC = {
  'a': 1,
  'b': '2',
  'c' : 3,
}
  • 의문? Record는 왜 안될까? => Record는 TS에서 제공하는 유틸리티 타입으로 단일 key type만을 지원함. 동적으로 key 타입을 다르게 하고 싶으면 mapped Type을 사용해야함.
type KeyTypes = {
a: string;
b: number;
c: boolean;
};

type MyObject = {
[K in keyof KeyTypes]: KeyTypes[K];
};

(TSPlayGround)

  • 요약
    • 런타임때까지 객체의 속성을 알 수 없을 때만 인덱스 시그니처를 사용하자
  • 안전한 접근을 위해 인덱스 시그니처의 값 타입에 undefined를 추가하는 것을 고려하자.
  • 가능하다면 인터페이스, Record 매핑된 타입과 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋다.

item16.number 인덱스 시그니처보다는 Array, 튜플, ArrayLike을 이용하기

  • 배열은 객체이다 => 그러므로 key값은 string인데, TS에서는 인덱스 시그니쳐로 버그를 잡기 위해 number형을 사용한다.

  • 인덱스 시그니처에 number를 사용하기 보다 Array나 튜플, ArrayLike Type을 사용하는게 좋다.

interface Array<T> {
  // ...
  [n: number]: T;
}

const xs = [1, 2, 3];
const x0 = xs[0];  // OK
const x1 = xs['1'];  // stringified numeric constants are also OK

const inputEl = document.getElementsByTagName('input')[0];
const xN = xs[inputEl.value];
//            ~~~~~~~~~~~~~ Index expression is not of type 'number'.

const tupleLike: ArrayLike<string> = {
  '0': 'A',
  '1': 'B',
  length: 2,
};  // OK

item17.변경관련된 오류방지를 위해 readonly 사용하기

  • 다음과 같은 코드가 있다고 가정하자.

  • arraySum에서 원본 배열을 변경해 문제가 발생 => 이럴땐 readonly 키워드를 사용(원본배열을 변경하지 못하도록)

  • 원본을 변경하지 못하도록 하는 키워드이다.

function printTriangles(n: number) {
  const nums = [];
  for (let i = 0; i < n; i++) {
    nums.push(i);
    console.log(arraySum(nums));
  }
} // num 에 push

function arraySum(arr: number[]) {
  let sum = 0, num;
  while ((num = arr.pop()) !== undefined) {
    sum += num;
  }
  return sum;
} // pop을 하기 때문에 => 이전의 수들이 유지가 되지 않음.

>printTriangles(5)
0
1
2
3
4
  • 기본적으로 readonly는 얕게 동작한다. Readonly 제너릭에도 동일하게 적용됨.

  • inner에 적용되는 것이지 x에 적용되는게 아니다. 깊은 readonly를 사용하려면 라이브러리를 사용하는게 낫다.

type T = Readonly<Outer>;
//   ^? type T = {
//        readonly inner: {
//          x: number;
//        };
//      }
profile
응애 나 애기 개발자

0개의 댓글