Archive

Effective Typescript - week 3 (item 26-27)

|

Effective Typescript - week 3 (item 26-27)


해당 포스트는 [이펙티브 타입스크립트](댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021) 책을 읽으며 정리한 내용입니다.

Item26. 타입 추론에 문맥이 어떻게 사용되는지 이해하기

type Language = 'Javascript' | 'Typescript';

function setLanguage(language: Language) {
  console.log(language);
}

let language = 'Javascript';
setLanguage(language); //Error: 'string' 형식의 인수는 'Language' 형식의 매개 변수에 할당될 수 없습니다.

런타임에서는 전혀 문제가 없는 동작이지만, 함수의 매개변수로 들어갈 값을 변수로 할당하니 오류가 발생한다.

이를 해결하기 위해 language에 타입을 선언하거나 const로 선언하여 변경할 수 없음을 알려준다.

let language: Language = 'Javascript';
setLanguage(language);
const language = 'Javascript';
setLanguage(language);


function calc(where: [number, number]) {}

const loc = [10, 20];
calc(loc); // Error: 'number[]' 형식의 인수는 '[number, number]' 형식의 매개 변수에 할당될 수 없습니다.
           // 대상에 2개 요소가 필요하지만, 소스에 더 적게 있을 수 있습니다.

튜플 사용 시 비슷한 문제가 발생하여 as const로 타입을 단언하여 해결한다.

function calc(where: [number, number]) {}

const loc = [10, 20] as const;
calc(loc); // Error: 'readonly [10, 20]' 형식의 인수는 '[number, number]' 형식의 매개 변수에 할당될 수 없습니다.
           // 'readonly [10, 20]' 형식은 'readonly'이며 변경 가능한 형식 '[number, number]'에 할당할 수 없습니다.

하지만 이번엔 다른 문제가 발생한다. as const로 loc을 불변이라고 정의하였지만, calc의 매개변수인 where는 불변을 보장하지 않는다.

function calc(where: readonly [number, number]) {}

const loc = [10, 20] as const;
calc(loc); // 정상

where에 readonly 구문을 추가하여 불변을 유지하였다. 타입 오류는 발생하지 않지만, loc의 배열 길이가 3이 되면?

실제 문제는 loc에서 발생하였지만, calc 함수를 사용한 곳에서 오류가 발생하므로 오류 파악이 힘들어진다.


function callWithCB(fn: (n1: number, n2: number) => void) {
  fn(1, 2);
}
callWithCB((a, b) => a + b);

a와 b는 number로 추론이 된다. 콜백을 상수로 뽑아내면 문맥이 소실되고 a, b에 any 타입이 허용되어 오류가 난다.

function callWithCB(fn: (n1: number, n2: number) => void) {
  fn(1, 2);
}
const fn = (a, b) => a + b; // Error: 'a', 'b' 매개 변수에는 암시적으로 'any' 형식이 포함됩니다.
callWithCB(fn);

a와 b에 타입을 선언하여 문제를 해결한다.

function callWithCB(fn: (n1: number, n2: number) => void) {
  fn(1, 2);
}
const fn = (a: number, b: number) => a + b; // 정상
callWithCB(fn);


Item27. 함수형 기법과 라이브러리로 타입 흐름 유지하기

내장된 함수형 기법과 lodash 등을 이용하여 타입 흐름을 유지하고 가독성을 높일 수 있다.

import _ from "lodash";

const users = [
  { id: 1, name: "v1" },
  { id: 2, name: "v2" },
  { id: 3, name: "v3" },
  { id: 4, name: "v4" },
];

const result = _.chain(users)
  .sortBy("id")
  .map((i) => {
    return i.name + " hi";
  })
  .head()
  .value();

console.log(result); // v1 hi

lodash도 체이닝해서 사용할 수 있다는 것을 처음 알았다..

확실히 타입 선언을 따로 안해도 알아서 추론을 해주니 편하긴 하지만, lodash를 굳이 사용해야하는지는 회사에서도 논란이 많았고, lodash 자체가 번들 사이즈도 큰 편이어서 이 부분은 좀 고민이 필요해보인다 👀



참고 자료


이펙티브 타입스크립트 - 댄 밴더캄 지음

Effective Typescript - week 3 (item 24-25)

|

Effective Typescript - week 3 (item 24-25)


해당 포스트는 [이펙티브 타입스크립트](댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021) 책을 읽으며 정리한 내용입니다.

Item24. 일관성 있는 별칭 사용하기

별칭은 타입 좁히기를 방해하므로 별칭 사용 시 일관되게 사용해야한다

interface Axis {
  x: number;
  y: number;
}

interface Flag {
  x: [number, number];
  y: [number, number];
}

interface Coordinate {
  log: Axis[];
  lan?: Flag;
}

function calc(coor: Coordinate) {
  const flag = coor.lan;
  if (coor.lan) {
    const temp = flag?.x[0] + flag?.y[0]; // Error: 개체가 'undefined'인 것 같습니다.
  }
}

coor.lan으로 잘 동작하던 코드가 flag라는 별칭을 만들어 사용하는 순간 편집기에 오류가 표시된다.

flag와 coor.lan의 타입은 Flag | undefined가 되기 때문이다.

if문의 체크를 다음과 같이 바꿔주면 ‘별칭은 일관성 있게 사용한다’는 원칙을 준수하기에 오류가 없어진다.

function calc(coor: Coordinate) {
  const flag = coor.lan;
  if (flag) {
    const temp = flag?.x[0] + flag?.y[0]; // 정상
  }
}

굳이 별칭을 쓰지 않고 비구조화 할당을 사용하여 더 간결하게 표기한다.

function calc(coor: Coordinate) {
  const { lan } = coor;
  if (lan) {
    const { x, y } = lan;
  }
}

주의할 점은 x와 y가 선택적 속성일 경우 null 속성 체크가 필요하다.


Item25. 비동기 코드에는 콜백 대신 async 함수 사용하기

콜백보다 프로미스를 사용할 경우, 타입스크립트가 추론을 더 잘한다

async function getItem() {
  const [res1, res2, res3] = await Promise.all([
    fetch("1"),
    fetch("2"),
    fetch("3"),
  ]);
}
// res1, res2, res3 Type: Response


함수가 프로미스를 반환한다면, async로 선언하는 것이 좋다

async function getData(url: string): Promise<any> {
  const res = await fetch(url);
  const json = await res.json();
  return json;
}

async 함수에서 프로미스 반환 시, 또 다른 프로미스로 래핑되지 않는다.

Promise<Promise<T>>가 아닌 Promise<T>가 된다.



참고 자료


이펙티브 타입스크립트 - 댄 밴더캄 지음

Effective Typescript - week 3 (item 22-23)

|

Effective Typescript - week 3 (item 22-23)


해당 포스트는 [이펙티브 타입스크립트](댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021) 책을 읽으며 정리한 내용입니다.

Item22. 타입 좁히기

타입 좁히기는 넓은 타입으로부터 좁은 타입으로 진행하는 과정이다. 주로 if문의 null 타입 체크

const el = document.querySelector('.foo'); // Type: HTMLElement | null
if (el) { // Type: HTMLElement
  //...
} else { // Type: null
  //...
}

if 문의 첫 번째 블록에서 null을 제외하므로 타입이 좁혀졌다.

자바스크립트에서는 typeof null === 'object'이므로 주의할 것.

const el = document.querySelector('.foo'); // Type: HTMLElement | null
if (typeof el === 'object') { // Type: HTMLElement | null??
  //...
}

마찬가지로 빈 문자열 ‘’ 과 0 모두 false이므로 타입좁히기 시 주의해야한다.


타입을 좁히기 위해 명시적으로 태그를 사용할 수 있다. 태그된 유니온 구별된 유니온이라 부른다.
interface Upload {
  type: 'upload';
  filename: string;
  contents: string;
}
interface Download {
  type: 'download';
  filename: string;
}
type AppEvent = Upload | Download;
function handleEvent(e: AppEvent) {
  switch (e.type) {
    case 'upload':
      //...
    case 'download':
      //...
  }
}


타입스크립트의 타입 식별을 돕기 위해 ‘타입 가드’를 사용할 수 있다.

const wanted = ["frontEnd", "backEnd", "devOps"];
const departments = ["HR", "frontEnd"].map((a) => wanted.find((n) => n === a));
// departments Type : const departments: (string | undefined)[]

여기서 undefined를 없애려해도 잘 안될 때, 타입가드를 사용한다.

function isDefined<T>(x: T | undefined): x is T {
  return x !== undefined;
}
const departments = ["HR", "frontEnd"]
	.map((a) => wanted.find((n) => n === a))
	.filter(isDefined);
// departments Type : const departments: string[]


Item23. 한꺼번에 객체 생성하기

객체를 생성할 때, 여러 속성을 넣어서 한 번에 생성해야 타입 추론에 유리하다.

const pt = { x: 1, y: 2 };
const db = { id: '123' };
const obj = {};
Object.assign(obj, pt, db);
obj.id; // Error: '{}' 형식에 'id' 속성이 없습니다.

spread 연산자를 사용하면 타입 오류 없이 객체를 생성할 수 있다.

const pt = { x: 1, y: 2 };
const db = { id: "123" };
const obj = { ...pt, ...db };
// obj Type : const obj: { id: string; x: number; y: number; }

타입에 안전한 조건부 속성을 추가하려면 빈 객체 {} 또는 null을 이용한다.

declare let isOk: boolean;
const term = { first: 'lim' };
const obj = { ...term, ...(isOk ? { second: 'two' } : {})};
// obj Type : const obj: { second?: string | undefined; first: string; }



참고 자료


이펙티브 타입스크립트 - 댄 밴더캄 지음

Effective Typescript - week 3 (item 19-21)

|

Effective Typescript - week 3 (item 19-21)


해당 포스트는 [이펙티브 타입스크립트](댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021) 책을 읽으며 정리한 내용입니다.

Item19. 추론 가능한 타입을 사용해 장황한 코드 방지하기

타입 구문을 일일이 적는것은 때로는 비생산적이라고 느낄때가 있다.

마침 이번 장에서 그에 대한 내용을 설명하고 있다.

타입스크립트가 추론 가능하다면 타입 구문을 작성하지 않는것이 좋다

interface Product {
  id: string;
  price: number;
}

function log(product: Product) {
  const id: string = product.id;
  const price: number = product.price;
}

여기서 id가 number로 바뀌었다고 가정했을 때, log 함수 내의 id 지역변수도 타입을 일일이 수정해주어야한다.

function log(product: Product) {
  const { id, price } = product;
}

id는 string, price는 number로 추론이 가능하기 때문에 굳이 적어주지 않아도 된다. 매개변수에 이미 타입을 명시했기 때문에 이 정도로도 충분하다.


함수의 시그니처에는 타입 구문이 있지만, 함수 내 지역변수에는 타입 구문이 없는것이 이상적이다

매개변수에 기본값이 주어지는 경우엔 타입이 추론이 되기 때문에 타입 구문을 적지 않을 때도 있다.

또, 타입 정보가 있는 라이브러리의 콜백 함수에서 매개변수 타입은 자동으로 추론된다.

// express
app.get('/api', (req: express.Request, res: express.Response) => {
  // X 잘못된 사례
});

app.get('/api', (req, res) => {
  // O 자동 추론이 되므로 타입 구문 필요 없다
});


객체 리터럴, 함수 반환 시에는 타입 명시를 고려해볼 필요가 있다

객체 선언 시에 타입을 명시해서 잉여속성 체크의 이점을 누릴 수 있다.

또, 타입 구문이 없으면 객체를 사용한 곳에서 오류가 발생하지만,

타입이 있으면 객체를 선언한 부분에서 오류가 발생해서 디버깅에 용이하다.

interface Product {
  id: string,
  title: string,
}

const a = {
  id: 123,
  title: 'abc',
};

function log(product: Product) {
  const { id, title } = product;
}

log(a); // Error: 'number' 형식은 'string' 형식에 할당할 수 없습니다.


함수 반환 시에도 타입을 명시하여 오류를 방지할 수 있다. 더 직관적으로 타입 확인이 가능하다.

interface Vector2D {
  x: number;
  y: number;
}

// function add(a: Vector2D, b: Vector2D): Vector2D
function add(a: Vector2D, b: Vector2D): Vector2D {
  return {
    x: a.x + b.x,
    y: a.y + b.y,
  };
}

// function add(a: Vector2D, b: Vector2D): { x: number; y: number; }
function add(a: Vector2D, b: Vector2D) {
  return {
    x: a.x + b.x,
    y: a.y + b.y,
  };
}


Item20. 다른 타입에는 다른 변수 사용하기

일반적으로 자바스크립트 let 키워드의 변수는 다른 타입의 값도 담아서 재사용이 가능하지만,

타입스크립트에서는 오류가 발생한다.

let a = '123';
a = 123; // Error: 'number' 형식은 'string' 형식에 할당할 수 없습니다.

이를 방지하기 위해 유니온 타입을 사용할 수 있다.

let a: string|number = '123';
a = 123;

하지만, a가 복잡한 타입이라면 이를 일일이 확인하는 것이 더 비효율적일 수 있다.

차라리 타입이 다르다면 변수를 분리하는 것이 좋다. 또, let 대신 const의 사용으로 타입 체커가 타입을 추론하기에 더 좋다.


Item21. 타입 넓히기

let, var 등 변경 가능한 변수로 선언하고 타입을 명시하지 않으면, 그 변수의 타입이 리터럴 값이 속한 기본 타입으로 넒혀진다.

이를 타입 넒히기(type widening)라 한다. 타입을 명시하여 넒히기를 방지할 수 있다.

let x = 'x';
function (axis: 'x'|'y') {
  return [axis];
}
add(x); // Error: 'string' 형식의 인수는 '"x" | "y"' 형식의 매개 변수에 할당될 수 없습니다.

런타임에서는 문제가 없지만, x의 타입이 string으로 넓혀져서 타입스크립트에서는 오류가 발생한다.

const로 정의하여 리터럴 값을 타입으로 정하던지, 타입을 명시하여 해결할 수 있다.

하지만, const도 모호한 순간이 있다.

const x = {
  v: 1,
};

x.v = 3;
x.v = '3'; // Error: 'string' 형식은 'number' 형식에 할당할 수 없습니다.
x.a = '123'; // Error: { v: number; }' 형식에 'a' 속성이 없습니다.

x의 타입은 { v: number }로 추론되었다. 따라서, v에 string 형식을 할당할 수도, 다른 속성을 추가할 수도 없다.


  • as const (const assertion)

as const로 타입을 좁히고 넓히기 동작을 막을 수 있다.

// const x2: { v: 1; }
const x2 = {
  v: 1 as const,
};

// const x3: { readonly v: 1; }
const x3 = {
  v: 1,
} as const;



참고 자료


이펙티브 타입스크립트 - 댄 밴더캄 지음

Effective Typescript - week 2 (item 16-18)

|

Effective Typescript - week 2 (item 16-18)


해당 포스트는 [이펙티브 타입스크립트](댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021) 책을 읽으며 정리한 내용입니다.

Item16. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용

타입스크립트는 자바스크립트와 다르게 숫자로 된 키를 허용하고 문자열로 된 키와 다르게 인식된다.

자바스크립트로 트랜스파일되면 어차피 문자열로 바뀌게 되지만, 적어도 타입 체크 시점에서는 유용하다.

const x = [1,2,3];
const x0 = x[0];
const x1 = x['1']; // Error: 인덱스 식이 'number' 형식이 아니므로 요소에 암시적으로 'any' 형식이 있습니다.


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

readonly 접근 제어자를 붙인 readonly number[] 는 타입으로 분류되어 일반 number[]와 구분되는 특징이 있다.

  • 배열의 요소를 읽을 수 있지만, 쓸 수 없다
  • length를 읽을 수는 있지만, 바꿀 수 없다
  • 배열을 변경하는 Array Method를 호출할 수 없다

number[]는 readonly number[]의 서브타입이 된다. 따라서, 변경 가능한 배열을 readonly 배열에 할당할 수 있다.

하지만, 그 반대는 될 수 없다

const a: number[] = [1,2,3];
const b: readonly number[] = a;
const c: number[] = b; // Error : 'readonly number[]' 형식은 'readonly'이며 변경 가능한 형식 'number[]'에 할당할 수 없습니다.


함수가 매개변수를 바꾸지 않는다면 readonly를 선언해야한다. 그런데 함수를 readonly로 만들면 그 함수를 호출하는 다른 함수도 readonly로 만들어야한다. 그래야 타입 안정성이 보장된다.

인덱스 시그니처에서도 readonly로 속성이 변경되는것을 방지할 수 있다.

let obj: Readonly<{ [k: string]: number }> = {};
obj.a = 1; // Error : 'Readonly<{ [k: string]: number; }>' 형식의 인덱스 시그니처는 읽기만 허용됩니다.
obj = { ...obj, a: 1 }; // 정상


readonly는 shallow하게 동작하므로 deep하게 사용되길 원하면 ts-essentials의 DeepReadonly 제네릭을 사용하면 된다.


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

특정 인터페이스의 속성들과 동기화된 새로운 값을 만들려면 이를 매핑시켜서 사용한다.

훗날 이 인터페이스에 새로운 속성이 추가될 때, 타입체크에 의해 오류를 방지할 수 있다.

다음은 렌더링 최적화를 위해 함수 타입은 shoudUpdate를 건너뛰기 위해 사용되는 코드이다.

interface Product {
  id: string;
  price: number;
  title: string;
  label: string;
  onClick: () => void;
}

const PRODUCT_UPDATE: { [k in keyof Product]: boolean } = {
  id: true,
  price: true,
  title: true,
  label: true,
  onClick: false,
}

function shouldUpdate(oldProps: Product, newProps: Product) {
  let k: keyof Product;
  for (k in oldProps) {
    if (oldProps[k] !== newProps[k] && PRODUCT_UPDATE[k]) {
      return true;
    }
  }
  return false;
}

[k in keyof Product]: boolean 이렇게 매핑시켜놓으면 나중에 Product에 속성이 추가되었을때 PRODUCT_UPDATE도 추가하도록 강제할 수 있다.



참고 자료


이펙티브 타입스크립트 - 댄 밴더캄 지음