Archive

Effective Typescript - week 2 (item 14-15)

|

Effective Typescript - week 2 (item 14-15)


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

Item14. 타입 연산과 제네릭 사용으로 반복 줄이기

extends 키워드나 인터섹션 연산자로 반복 줄이기

interface Person {
  name: string;
  age: number;
}

interface PersonWithBirth extends Person {
  birth: Date;
}
type PersonWithDate = Person & { birth: Date };


제네릭 타입 사용하기

  • Pick
interface State {
  id: string;
  title: string;
  files: string[];
  content: string;
}

type NavState = Pick<State, 'id' | 'title' | 'content'>; // { id, title, content }
interface SaveAction {
  type: 'save'
}
interface LoadAction {
  type: 'load'
}
type Action = SaveAction | LoadAction;
type ActionType = Pick<Action, 'type'>; // { type: 'save' | 'load' }
  • Partial
interface Address {
  email: string;
  address: string;
}
type MyEmail = Partial<Address>; // email?, address? 선택적으로 가질 수 있다, 다 없어도 됨
interface Product {
  id: number;
  price: number;
  title: string
}

interface updateProduct { // 잘못된 예시
  id?: number;
  price?: number;
  title?: string;
}

function update(item: Partial<Product>) { // id, price, title 어떤 것이든 인자로 받을 수 있다
  // ...
}
  • Omit
interface Product {
  id: number;
  title: string;
  price: number;
  desc: string
}

type item = Omit<Product, 'desc'>; // { id, title, price }만 가진다
  • ReturnType
function getUserInfo(userId: string) {
  // ...
  return {
    userId,
    name,
    age,
    color,
  };
}

typeof UserInfo = ReturnType<typeof getUserInfo>;


타입 연산자 사용하기

  • typeof
const OPTION = {
  width: 640,
  height: 480,
  color: '#FFF',
  label: 'label'
};

type Options = typeof OPTION; // { width, height, color, label }


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

타입에 인덱스 시그니처를 사용하여 유연하게 매핑할 수 있다

type Product = { [property: string]: string };
const item: Product = {
  category: "스낵",
  label: "과자",
};

[키의 이름: 키의 타입]: 값의 타입으로 사용된다

어떤 이름이건 키의 이름으로 들어갈 수 있고, 키 마다 다른 타입의 값을 가질 수 없다.

또, 자동완성 기능을 제공받지 못하는 단점이 있다.

런타임 때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 사용하자.

가능하다면 정확한 타입을 사용하라는 댄 형님의 조언 👊



참고 자료


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

Effective Typescript - week 2 (item 11-13)

|

Effective Typescript - week 2 (item 11-13)


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

Item11. 잉여 속성 체크의 한계 인지하기

interface Wanted {
  id: number;
  name: string;
}

const me: Wanted = {
  id: 1,
  name: 'limu',
  position: 'front-end', // Error: 객체 리터럴은 알려진 속성만 지정할 수 있으며 'Wanted' 형식에 'position'이(가) 없습니다.
};

// --------------------------------

const obj = {
  id: 1,
  name: 'limu',
  position: 'front-end',
};

const i: Wanted = obj; // 정상

첫번째 예시에서 잉여 속성 체크가 수행되어 타입 Wanted에 포함되지 않은 속성이 에러로 잡혔다.

두번째 예시에서 obj 타입은 id, name, position을 모두 포함한 객체가 되고 이 객체는 Wanted 타입의 부분집합이므로 Wanted에 할당이 가능하기 때문에 타입체커를 통과했다.

잉여 속성 체크 할당 가능 검사는 별도의 과정인 것을 알 수 있다.

객체 리터럴을 변수에 직접 할당할 때, 잉여 속성 체크가 수행된걸 알 수 있다.

또, 함수에 매개변수 전달 시에도 잉여 속성 체크가 수행된다.


const o = { id: 1, name: 'limu', position: 'front' } as Wanted; // 정상

타입 단언문의 경우에도 역시 타입체커를 통과했다. 단언문의 사용은 지양해야된다는 예시.


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

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

type Fn = (num: number) => number;
const aa: Fn = num => { //... };


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;

깔~끔 👍


Item13. 타입과 인터페이스의 차이점 알기

유니온 타입은 가능하지만 유니온 인터페이스는 불가능하다

type AB = 'A' | 'B';
interface aa extends AB { // Error: 인터페이스는 개체 형식 또는 정적으로 알려진 멤버가 포함된 개체 형식의 교집합만 확장할 수 있습니다.
  name: string;
}

type able = 'A' | 'B' & { name: string }; // 타입은 이렇게도 가능


튜플, 배열도 type으로 깔끔하게 표현이 가능하다

type TTuple = [number, number];

interface ITuple {
  0: number,
  1: number,
  length: 2,
};


반면 인터페이스는 타입에 없는 기능이 있다.

interface IState {
  name: string;
}

interface IState {
  age: number;
}

const who: IState  = {
  name: 'lim',
  age: 19,
}

인터페이스의 속성을 확장하는 선언 병합이 그 중 하나이다.


API에 대한 타입을 작성하는 경우 향후 명세가 바뀔 것을 고려한다면 선언 병합이 가능한 인터페이스로 구현하는 것이 좋다.

그러나, 프로젝트 내부에서 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계이므로 타입, 인터페이스 잘 선택하라는 댄 형님의 조언 👊



참고 자료


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

Effective Typescript - week 2 (item 09-10)

|

Effective Typescript - week 2 (item 09-10)


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

Item09. 타입 단언보다는 타입 선언을 사용하기

변수에 값을 할당하고 타입을 부여하는 방법은 두 가지가 있다

interface Person {
  name: string;
};

const foo: Person = { name: 'foo' }; // 타입 선언
const poo = { name: 'poo' } as Person; // 타입 단언

타입 선언은 해당 값이 인터페이스를 만족하는지 체크하지만, 타입 단언은 강제로 ‘이 타입이다’라고 간주하기 때문에 타입 체크를 무시한다.

const sin: Person = {}; // Error: 'name' 속성이 '{}' 형식에 없지만 'Person' 형식에서 필수입니다.
const cos = {} as Person; // 정상

속성 추가시에도 적용된다. 타입 선언으로 지정한 객체에 속성 추가시, 해당 속성이 interface에 없으면 오류가 나지만, 타입 단언의 경우에는 그냥 지나간다.


화살표 함수를 사용할 때, 타입 추론으 모호할 때가 있다.

interface MyNumber {
  number: string;
};

const num = ['1', '2', '3'].map((number) => ({ number }));
// const num: { number: string }[]

num의 타입이 { number: string }[]이 된다. MyNumber[] 타입을 원한다고 다음과 같이 작성할 수 있다.

const num = ['1', '2', '3'].map((number) => ({ number } as MyNumber));
// const num: MyNumber[]

위에서처럼 반환되는 값이 {} as MyNumber여도 타입 체크는 오류를 뱉지 않는다.

const num: MyNumber[] = ['1', '2', '3'].map((number): MyNumber => ({ number }));

이렇게 (number): MyNumber number의 타입은 없지만 반환되는 타입을 지정한다.

(number: MyNumber)라고 작성하면 number의 타입은 있지만 반환 타입이 없어서 오류가 난다.


타입스크립트는 DOM에 접근할 수 없기 때문에 이런 경우에는 타입 단언이 활용된다.

document.querySelector('.Button').addEventListener('click', (e) => {
  const button = e.currentTarget as HTMLButtonElement;
});


접두사에 붙이는 ! (확정 할당 어선셜)의 경우에도 타입 단언이라고 볼 수 있다. null이 아님이 확실할 때 사용한다. 아니면 일일이 null 타입 체크를 다 넣어야할지도?


Item10. 객체 래퍼 타입 피하기

원시타입 string은 그 안에 메서드를 가지고 있는 것처럼 보이지만, 실제로는 String 객체로 래핑하고 메서드를 호출하고 마지막에 래핑한 객체를 버리는 식이다.

이 객체 래퍼는 자기 자신 외에는 모두 같지 않다.

'hi' === new String('hi'); // false
new String('hi') === new String('hi'); // false

string은 String에 할당 가능하지만 그 반대의 경우는 성립되지 않는다.

따라서, 타입 선언 시에 잘못 타이핑하지 않도록 주의하자는 댄 형님의 조언 👊



참고 자료


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

Effective Typescript - week 1 (item 06-08)

|

Effective Typescript - week 1 (item 06-08)


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

Item06. 편집기를 사용하여 타입 시스템 탐색하기

타입스크립트를 설치하면 컴파일러(tsc)와 타입스크립트 서버(tsserver)를 실행할 수 있다.

타입스크립트 서버도 ‘언어 서비스’를 제공하는데 여기에는 코드 자동완성, 스펙 검사, 검색, 리펙토링 등이 포함된다.

특히 타입이 동봉되지 않은 외부 라이브러리에 대해 @types 패키지를 설치하라는 인텔리센스를 지원받을 수 있다.

해당 기능을 끄고 싶으면 vs code setting에서 아래 항목을 true로 설정한다.

인텔리센스


언어 서비스는 라이브러리의 타입을 쉽게 탐색할 수 있도록 도와준다. (F12: 정의)

fetch

fetch type


Item07. 타입이 값들의 집합이라고 생각하기

타입은 값의 집합이다

타입의 세계에서 가장 작은 집합은 never 타입이다. never 타입은 공집합이므로 아무런 값도 할당받을 수 없다.

const x: never = 12;
// 'number' 형식은 'never' 형식에 할당할 수 없습니다.

그 다음은 한 가지 값만 포함하는 unit 타입이라고도 불리는 literal 타입이다.

type name = 'limu';
type B = 'B';

둘 이상으로 묶으려면 union 타입을 사용한다.

type AKB48 = 'A' | 'K' | 'B' | 48;


타입은 엄격한 상속관계가 아닌 겹쳐지는 집합이다

type AKB = 'A' | 'K' | 'B';

const akb: AKB = Math.random() < 0.5 ? 'A' : 'B';
// A와 B는 AKB 타입에 속하므로 정상


한 객체의 추가 속성이 타입 선언에 없더라도 그 타입에 속할 수 있다

interface Person {
  name: string;
}

interface Life {
  birth: Date;
  death?: Date;
}

type PersonLife = Person & Life;

const human: PersonLife = {
  name: 'foo',
  birth: new Date('1992/01/03'),
}

type temp = keyof (Person | Life);

const human3: temp = {
  name: 'bar', // Error: 'string' 형식은 'never' 형식에 할당할 수 없습니다.
  birth: new Date('1992/01/03'), // Error: 'Date' 형식은 'never' 형식에 할당할 수 없습니다.
}

type temp2 = keyof (Person & Life);

const human4 = 'name' // 정상

Person과 Life의 교집합이 없으므로 never로 가는것을 생각하지만, 타입 연산자의 동작은 그렇지 않다.

& 인터섹션 타입의 값은 타입 내의 속성을 모두 포함하는 것이 일반적. 타입 정의를 하나로 합치는 동작을 한다.

반대로, 유니온 타입에 대한 keyof 는 never가 된다. keyof는 기본적으로 객체의 key값을 타입으로 사용 지정하게 한다.

keyof (A&B) = (keyof A) | (keyof B) : key 값들 (‘name’, ‘birth’, ‘death’)

keyof (A|B) = (keyof A) & (keyof B) : key 값들 중 겹치는 값 (never)


  • 유니온 타입 작성 시 주의사항
interface Person {
  name: string;
}

interface Developer {
  name: string;
  birth: Date;
  death?: Date;
}

type PType = Person | Developer;

function hello(someone: PType) {
  someone.name;
  someone.birh; // Error: 'PType' 형식에 'birth'속성이 없습니다.
}

함수 호출 시점에 두 타입 중 어느 것이 올 지 모르므로 오류가 나이 않는 선에서 타입추론을 하게 된다.

따라서, 두 인터페이스 모두 가지고 있는 name 이외에는 접근할 수 없다.


타입 연산은 집합의 범위에 적용된다

extends 키워드로 부분집합을 생각해볼 수 있다.

interface Vector1D {
  x: number;
}

interface Vector2D extends Vector1D{
  y: number;
}

y이외에 반드시 x를 포함해야하기에, Vector2D는 Vector1D의 부분집합이라 볼 수 있다.


Item08. 타입 공간과 값 공간의 심벌 구분하기

타입과 값의 혼동이 없도록 주의하자. 이름 지을 때 중복이 있다면 아무래도 혼란스러울듯?

class와 enum은 상황에 따라 타입, 값 두 가지에 쓰이는 예약어이다.

연산자 중에서는 typeof가 타입, 값에 쓰일 때 각각 다른 기능을 한다.

interface Person {
  name: string;
}

const p: Person = {
  name: "foo",
};

type T1 = typeof p; // Person 타입

const b = typeof p; // 'object' 문자열



참고 자료


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

Effective Typescript - week 1 (item 04-05)

|

Effective Typescript - week 1 (item 04-05)


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

Item04. 구조적 타이핑에 익숙해지기

타입은 봉인되어 있지 않다 or 타입은 열려있다

자바스크립트는 덕 타이핑 기반으로 타입스크립트는 이를 모델링하여 구조적 타이핑을 사용한다.

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

function calcLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

const v: NamedVector = { x: 3, y: 4, name: "foo" };
console.log(calcLength(v));  // 5

NamedVector의 구조가 Vector2D에 호환되기 때문에 NamedVector만을 위한 별도의 함수를 만들지 않아도 calcLength를 호출할 수 있다. 이를 구조적 타이핑 (structural typing)이라고 한다.

이 구조적 타이핑으로 문제가 발생하기도 한다.

Vector2D만을 받기를 원했지만 x, y와 다른 값들을 가진 객체를 인자로 전달했을때 타입 체커는 오류를 뱉지 않는다.

일반적으로 함수 작성 시, 인자로 들어올 값이 파라미터의 타입에 선언된 속성만을 가질거라 생각하지만, 타입은 열려있다.

봉인된 타입 혹은 정확한 타입을 위한 타입스크립트 설정도 가능하긴 하다.

타입이 열려있기 때문에 예기치 못한 에러와 만날 수도 있다.

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

function calcLength2(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis]; // <-- Error: 'string' 형식의 식을 'Vector3D' 인덱스 형식에 사용할 수 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
    length += Math.abs(coord);
  }
  return length;
}

const v2 = { x: 3, y: 4, z: 1, name: "string" };
console.log(calcLength2(v2));

v는 Vector3D의 인터페이스로 들어오기 때문에 x, y, z만 가질거라 생각하고 함수를 작성했지만, 타입은 열려있기에 v2 처럼 x,y,z를 모두 가지고 다른 타입도 가진 객체를 던졌을 때, 문제가 발생할 수 있다. 이를 타입스크립트가 미연에 잡아준 것 !

위 코드를 정확히 하기 위해선 모든 키를 루프를 돌기보단 정확히 필요한 값만 꺼내어 쓰는 것이 좋다.

타입스크립트 사용시 주의할 점은 늘어나겠지만, 결국 런타임 에러를 방지할 수 있다는 점이 강려크하긴 한듯 😇


Item05. any 타입 지양하기

프로토타입 코드 작성시, 일단 any로 작성하고 나중에 바꿔야지?? 라고 생각하여 any를 써두거나 타입을 지정하기 귀찮아서 as any로 타입을 단언해버리거나…

이처럼 any는 아주 편리한것처럼 보이지만, 편리한만큼 지게 될 리크스도 높아진다는 점을 생각해야한다.

any 타입에는 타입 안정성이 없다

let mustNumber: number;
mustNumber = "가나다" as any;
mustNumber += 1;
console.log(mustNumber); // 가나다1 ..?!?!

이렇듯 코드는 점점 산으로 가게 되고…


any는 함수 시그니처를 무시해버린다

다시 말해, 함수의 파라미터로 특정 타입의 인자를 받고 특정 타입의 값을 반환한다는 약속을 어기는 것이다.

function mustReturnNumber(d: Date): number {
  //...
}
const birthDate: any = '1992-01-03';
mustReturnNumber(birthDate); // <-- 타입 체커에 걸리지는 않지만 의도하지 않은 동작이 나올수도

이렇듯 코드는 또 다시 산으로 가게 되고…


any 타입에는 언어 서비스가 적용되지 않는다

즉, any를 남발하다보면 타입스크립트가 ‘그래 니 맘대로 해라’ 하고서 자동완성 기능 등 인텔리센스를 제공하지 않는다 !


이 밖에도 any 타입을 객체에 사용하면 타입 설계를 감추게 되므로, 이후 작업자가 코드를 이해하는데 상당한 시간을 쏟게 될 것이다. 또, 타입 체커를 의도적으로 패스하게 되므로 당장은 편하겠지만 그만큼 코드 신뢰도가 떨어지게 된다.

무조건적으로 나쁘다는것 보다 써야될 상황 또, 그렇지 않은 상황을 적절히 판단할 수 있는 눈을 가지라는 댄 형님의 조언 👊



참고 자료


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