이펙티브 타입스크립트

SeoYng·2023년 1월 15일
2
post-thumbnail

[ 🍫 사내스터디-2023.01 ~ 03]

📖 1장 타입스크립트 알아보기

☻ 1 타입스크립트와 자바스크립트의 관계 이해하기

  • 타입스크립트는 자바스크립트로 컴파일되며 실행
  • 타입 시스템 목표 중 하나는 오류를 발생시킬 코드를 미리 찾아내는 것
    => 정적타입 시스템
  • 단, 타입체커를 통과하면서도 런타임 오류를 발생시키는 코드 존재 가능
  • 모든 자바스크립트 프로그램은 이미 타입스크립트 프로그램

☻ 2 타입스크립트 설정 이해하기

  • StrictNullChecks: nullundefined가 모든 타입에서 허용되는지 확인
    => undefined는 객체가 아닙니다 같은 런타임 오류 방지

☻ 3 코드 생성과 타입이 관계없음을 이해하기

  • 타입스크립트 컴파일러의 두가지 일
    1) 자바스크립트로 트랜스파일
    2) 타입 오류 체크
  • 런타임에는 타입체크 불가능
  • 타입 연산은 런타임에 영향을 주지 않음
  • 타입스크립트의 타입은 런타임 성능에도 영향을 주지 않음

☻ 4 구조적 타이핑에 익숙해지기

☻ 5 any 타입 지양하기

  • 타입스크립트를 무력화시키므로 최대한 사용을 피해야함

📖 2장 타입스크립트의 타입 시스템

☻ 6 편집기를 사용하여 타입 시스템 탐색하기

🪄 유용한 ts관련 vscode 익스텐션

  • 타입스크립트 설치 시 컴파일러서버 사용 가능

☻ 7 타입이 값들의 집합이라고 생각하기

🤔❔ never은 언제 필요할까?
never 가이드

  • 불가능을 나타내는 타입이 필요 => 공집합
  • 타입스크립트에서는 불가능을 아래와 같이 다양한 방법으로 나타내고 있다.
    - 값을 포함할 수 없는 빈 타입
    1) 제네릭과 함수에서 허용되지 않는 매개변수
    2) 호환되지 않는 타입들의 교차 타입
    3) 빈 합집합(무의 합집합)
    - 실행이 끝날 때 호출자에게 제어를 반환하지 않는 함수의 반환 타입
    - 절대로 도달할수 없을 esle 분기의 조건 타입
    - 거부된 프로미스에서 처리된 값의 타입

  • 사용 케이스
/* 유니언 유형에서 멤버를 필터링 */
type Foo = {
  name: 'foo'
  id: number
}
type Bar = {
  name: 'bar'
  id: number
}
type All = Foo | Bar
type ExtractTypeByName<T, G> = T extends { name: G } ? T : never
type ExtractedType = ExtractTypeByName<All, 'foo'> // the result type is Foo
// type ExtractedType = {
//     name: 'foo';
//     id: number;
// }
/* mapped type에서 키를 필터링 하는 용도 */
type Filter<Obj extends Object, ValueType> = {
  [Key in keyof Obj as ValueType extends Obj[Key] ? Key : never]: Obj[Key]
}
interface Foo {
  name: string
  id: number
}
type Filtered = Filter<Foo, string> // {name: string;}
/* 제어 흐름에서 타입을 좁히고 싶을 때 */
function throwError(): never {
  throw new Error()
}
let foo: string | undefined
if (!foo) {
  throwError()
}
foo // string
  • 유닛타입(리터럴 타입): 한 가지 값만 포압하는 타입
  • 할당 가능한 : ~의 부분집합 관계
  • extends: ~에 할당가능한, ~의 부분 집합
  • 유니온 타입공통된 속성만 접근 가능
  • 인터섹션 타입의 값은 타입 내의 속성을 모두 포함하는 것이 일반적
// # Intersection 타입 문법
interface Developer {
  name: string;
  skill: string;
}

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

// NOTE: Union 타입 => 어떤 것이 들어올지 모르므로 공통된 속성만 접근가능
function askSomeone(someone: Developer | Person) {
  someone.name; // O
  // someone.age; // X
}

askSomeone({ name: '서영', skill: 'js' });
askSomeone({ name: '서영', age: 20 });

// NOTE: Intersection 타입 => 조합된 모든 속성만 접근가능
function askSomeone2(someone: Developer & Person) {
  someone.name; // O
  someone.age; // O
  someone.skill; // O
}

askSomeone2({ name: '서영', age: 20, skill: 'js' });

🤔❔ 표 2-1 인터섹션 / 유니온

  • 타입 연산자는 인터페이스의 속성이 아니라 값의 집합에 적용되어 헷갈리는 것
  • 교집합: T1 & T2 = keyof A | keyof B
    Developer & Person // name, age, skill
  • 합집합: T1 | T2 = keyof A & keyof B
    Developer | Person // name

🤔❔ Exclude

// 정의
type Exclude<T, U> = T extends U ? never : T;
// T에 오는 타입들 중 U에 오는 것들은 제외
type Fruit = "cherry" | "banana" | "strawberry" | "lemon";
type RedFruit = Exclude<Fruit, "banana" | "lemon">;
// type RedFruit = "cherry" | "strawberry"

☻ 8 타입 공간과 값 공간의 심벌 구분하기

  • 컴파일 과정에서 타입 정보는 제거됨

  • typeof

    타입
    타입스크립트 타입 반환런타임 연산자(string, number, function, object...)
interface Person {
  name: string; 
}
const p: Person = { name: 'seoyoung' };

type T = typeof p; // 타입은 Person
const v1 = typeof p; // 값은 'object'

🤔❓ class 48~50pg

class Cylinder {
   radius=1;
   height=1;
}
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
 shape // 정상, 타입은 Cylinder
 shape.radius // 정상, 타입은 number
}  
  • ❓ 48pg 마지막줄 "다음 예제에서 Cylinder 클래스는 타입으로 쓰였습니다"
    instanceofCyliner?
    - 47pg: instanceof는 런타임 연산자이고 값에 대한 연산을 함
    - ts playground 컴파일 결과: Cylinder 사라지지 않음
타입
속성과 메서드 사용생성자 사용
  • 클래스에 대한 typeof
const v = typeof Cylinder; // 값이 function; 클래스는 js에서 함수로 구현
type T = typeof Cylinder; // 타입이 class Cylinder, 즉 생성자 함수
const c = new fn(); // 타입이 Cylinder(인스턴스타입)
type C = InstanceType<typeof Cylinder>; // 타입이 Cylinder(인스턴스타입)

🤔❔ obj['field'] vs obj.field

  • 값은 동일하지만 타입은 다를 수 있음
  • 타입의 속성을 얻을 때는 반드시 obj['field'] 사용
interface Person { name: string };
type t1 = Person["name"] // string
type t2 = Person.name // 오류 
// Cannot access 'Person.name' because 'Person' is a type, but not a namespace. Did you mean to retrieve the type of the property 'name' in 'Person' with 'Person["name"]'?

🤔❔ 타입으로 쓰이는 this

  • 다형성의 this
  • 메서드 체인을 구현할 때 유용
export class calculator {
  constructor(public value: number =0){}
  add(value : number){
    this.value += value;
    return this
  }
  multiply(value : number){
    this.value *= value;
    return this
  }
}
import {Calculator} from './method-chain'
let calc = new Calculator;
let result = calc.add(1).add(2).multiply(3).value
  • "foo"는 문자열 리터럴이거나 문자열 리터럴 타입일 수 있음
const v1 = 'string literal'; // 문자열 리터럴
type T1 = 'string literal'; // 문자열 리터럴 타입

const s1: T1 = 'string'; // 오류
// type '"string"' is not assignable to type '"string literal"'.
const s2: T1 = 'string literal'; // 정상

☻ 9 타입 단언보다는 타입 선언을 사용하기

  • 타입단언 vs 타입선언
interface Person { name: string };

const p: Person = { name: 'seoyoung' }; // 타입 선언
const p = { name: 'seoyoung' } as Person; // 타입 단언
  • 타입단언은 강제로 타입을 지정했으니 타입 체커에게 오류를 무시하라고 하는 것
interface Person { name: string };

const alice: Person = {}; // name속성이 없습니다
const bob = {} as Person; // 오류 없음
  • 화살표 함수의 타입 선언
const people = ['alice', 'bob'].map(name => ({name}))
// { name: string }[]

// Person[]이지만 bad - 런타임 오류 발생 가능성
const people = ['alice', 'bob'].map(name => ({name} as Person))
const people = ['alice', 'bob'].map(name => ({} as Person)) // [{}, {}] 인데 오류 없음
// 안전하지만 복잡
const people = ['alice', 'bob'].map(name => {
  const person: Person = { name }
  return person
})

🤔❔ 화살표 함수의 반환타입을 명시하는 방법

// 소괄호가 중요 => name의 타입이 없고 반환타입은 Person
const people = ['alice', 'bob'].map(
  (name): Person => ({ name })
) // Person[]
// 방법2 - 더직관적
const makePersonList = (name: string): Person => ({ name })
const people = ['alice', 'bob'].map(makePersonList) // Person[]
  • 타입 체커가 추론한 타입 보다 개발자가 판단하는 타입이 더 정확할 때 타입 단언을 사용
    - DOM 조작
    - !. 단언문으로 null 이 아님을 단언

☻ 10 객체 래퍼 타입 피하기

  • 몽키패치: 런타임에 어떤 기능을 수정해서 사용하는 기법
  • 객체 래퍼 타입은 지양하고, 기본형 타입 사용 권장
  • new 없이 BigIntSymbol 호출시 기본형 생성

☻ 11 잉여 속성 체크의 한계 인지하기

  • 타입이 명시된 변수에 객체 리터럴 할당 시 그 외 속성이 없는지도 확인
  • 잉여 속성 체크(엄격한 객체 리터럴 체크)는 할당 가능 검사와 별도의 과정
// 리터럴 할당시 잉여속성 체크
interface Room {
 numDoors: number;
 ceilingHeightFt: number;
}

const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present', 
}
// 개체 리터럴은 알려진 속성만 지정할 수 있으며 'Room' 형식에 'elephant'이(가) 없습니다.
  
const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present', 
}

const r: Room = obj; // 정상
  • 잉여 속성 체크는 단언문을 사용할 때도 적용되지 않음
  • 잉여 속성 체크를 원하지 않을 때 인덱스 시그니처를 사용하여 추가적 속성을 예상하도록 할 수는 있음
interface Options {
	darkMode: boolean;
  	[otheroption: string]: unknown
}
const o: Options = { darkMode: true } // 정상
const o: Options = { darkMode: true, color: 'black' } // 정상
  • 함수에 매개변수로 전달할 때도 잉여속성 체크 수행

🤔❔ 약한타입(선택적 속성만 가짐) - 공통 속성 체크

interface LineChartOptions {
  logscale?: boolean;
  invertedYAxis?: boolean;
  areaChart?: boolean;
}
const opts = { logScale: true };
const o: LineChartOptions = opts;
   // ~ Type '{ logScale: boolean; }' has no properties in common
   //   with type 'LineChartOptions'
  • 잉여속성 체크와 다르게 약한 타입과 관련된 할당문마다 수행
const opts = { logScale: true };
const o: LineChartOptions = opts;
// Type '{ logScale: boolean; }' has no properties in common with type 'LineChartOptions'.
// 임시 변수를 제거하더라도 공통 속성 체크는 여전히 동작
const o: LineChartOptions = { logScale: true };
// Object literal may only specify known properties, but 'logScale' does not exist in type 'LineChartOptions'. Did you mean to write 'logscale'?

☻ 12 함수 표현식에 타입 적용하기

  • 타입스크립트에서는 재사용성 장점 때문에 함수 표현식을 사용하는 것이 좋음
const add = (a: number, b: number): number => a+b;
// ❗️ 함수 표현식 전체에 타입 구문을 적용하는 것이 좋음
type BinaryFn = (a: number, b: number) => number
const add: BinaryFn = (a, b) => a + b;

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

  • 인터페이스명 앞에 I를 붙이는 것은 현재는 지양해야할 스타일
  • 함수타입의 타입과 인터페이스 정의
type TFn = (a: number) => string
interface IFn = {
  (a: number) => string
}
  • 인터페이스와 타입은 서로 확장 가능
type TState = {
  name: string,
}
interface IState {
  name: string;
}
// IStateWithPop 와 TStateWithPop 는 동일
interface IStateWithPop extends TState {
  population: number;
}
type TStateWithPop = IState & { population: number; };
  • 인터페이스는 타입을 확장할 수는 있지만 유니온은 할 수 없음
  • type 키워드는 유니온이 가능하고 튜플과 배열타입을 더 간결하게 표현 가능
interface Pair {
  0: number;
  1: number;
  length: 2;
}

type Pair = [number, number];
  • type 키워드는 매핑된 타입 또는 조건부 타입 같은 고급 기능에 활용 되기도 함

  • interface 키워드는 보강이 가능

// 속성확장 - 선언 병합 (사용자가 채워야 하는 빈틈) 
interface IState {
  name: string;
}
interface IState {
  population: number;
}
const wyoming: IState = {
  name: 'wyoming',
  population: 50000
} // 정상
  • 따라서 프로퍼티가 추가되는 것을 원하지 않는다면 type 사용
  • 프로젝트 내부적으로 사용되는 타입에 선언병합이 발생하는 것은 잘못된 설계
  • API같이 변동이 있을 수 있는 것은 interface 사용

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

  • extends를 사용해서 인터페이스 반복을 피해야 함
  • keyof는 타입을 받아서 속성타입의 유니온을 반환
interface Options {
  width: number;
  height: number;
  color: string;
}
type OptionsKeys = keyof Options; // 'width' | 'height' | 'color'
  • Pick 어떤 정의된 객체 형태의 타입에서 특정한 프로퍼티들만 골라서 새로운 타입 생성
// 정의
type Pick<T, K extends keyof T> = {
	[k in K]: T[K]
}
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};
  • Partial 모든 프로퍼티를 선택적으로 만드는 타입을 구성
// 정의
type Partial<T> = {
  [P in keyof T]?: T[P];
}
type OptionsUpdate = Partial<Options>;

interface OptionsUpdate { // 기존의 Options타입과 동일하면서 선택적 필드
  width?: number;
  height?: number;
  color?: string;
}
// keyof와 매핑된 타입으로 동일하게 생성 가능
type OptionsUpdate = { [k in keyof Options]?: Options[k] };
  • ReturnType 함수 T의 반환 타입으로 구성된 타입 생성
type T0 = ReturnType<() => string>;  // string
type T1 = ReturnType<(s: string) => void>;  // void
type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]

declare function f1(): { a: number, b: string }
type T4 = ReturnType<typeof f1>;  // { a: number, b: string }
  • 타입 정의를 먼저하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋음

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

  • 런타임 때까지 객체의 속성을 알수 없을 경우에만 인덱스 시그니처 사용
    (ex: CSV파일 로드)
  • 인덱스 시그니처를 명시하여 유연하게 매핑을 표현
type Roket = {
	[propertiy: string]: string
}
const roket: Roket = {
	name: 'sally',
  	thrust: '4,940 kN'
}
// [키의 위치만 표시하는용도: 보통 string 사용]: 어떤 것이든 가능
  • 인덱스 시그니처의 단점
    - 잘못된 키를 포함해 모든 키를 허용한다. name 대신 Name도 가능
    - 특정 키가 필요하지 않다. {} 도 유효
    - 키마다 다른 타입을 가질 수 없음. thrust는 number여야 할수도 있음
    - 언어 서비스를 제공받을 수 없음

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

  • 배열은 객체이므로 키는 숫자가 아닌 문자열
  • key로 숫자를 사용하면 자바스크립트 런타임은 문자열로 변환
> {1: 2, 3: 4}
{'1': 2, '3': 4}
  • 타입스크립트는 숫자키를 허용하고 문자열 키와 다른 것으로 인식
  • 인덱스 시그니처로 사용된 nunmber타입은 버그를 잡기위한 순수 타입스크립트 코드
interface Array<T> {
	// ...
	[n: number]: T;
}
  • number를 인덱스 타입으로 사용하면 숫자 속성이 어떤 특별한 의미를 지닌다는 오해를 불러 일으킬 수 있음
const arr = [1, 2, 3]; // 숫자로 인덱스할 항목을 지정하는 경우 배열을 사용
  • 타입이 불확실하다면 for-in 루프는 느림
  • 길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶다면 ArrayLike타입을 사용
    (ex: map, forEach 같은 배열 고차함수를 사용하고 싶지 않을때)
  • ArrayLike를 사용하더라도 키는 여전히 문자열
const tupleLike: ArrayLike<string> = {
 '0': 1,
 '1': 2,
 length: 2,
}; // 정상

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

  • 함수가 매개변수를 변경하지 않는다면 - readonly는 얕게(shallow) 동장로 선언
  • readonly는 얕게(shallow) 동작
interface Outer {
  inner: {
    x: number;
  }
}
const o: Readonly<Outer> = { inner: { x: 0 } };
o.inner = { x: 1 }; // 오류
o.inner.x = 1; // 정상

🤔❔ const vs readonly

// 변경 가능성을 옮긴 것
const arr1: number[] = [3]
arr1 = [3, 4] // Cannot assign to 'ac' because it is a constant
// 가리키는 배열을 자유럽게 변경할 수 있지만 배열 자체는 변경 못함
let arr2: readonly number[] = [3]
arr2 = [3, 4]

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

  • 매핑된 타입을 사용하여 관련된 값과 타입을 동기화 하도록 하는 것이 좋음
// ScatterProps 가 변경되면 REQUIRES_UPDATE 도 변경됨을 알림
const REQUIRES_UPDATE: {[k in keyof ScatterProps]: boolean} { 
  xs: true,
  ys: true,
  xRange: true,
  yRange: true,
  color: true,
  onClick: false,
};

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;
  }
}
  • 실패에 닫힌 방법은 오류 발생 시 적극적으로 대처하는 방향

📖 3장 타입 추론

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

  • 모든 변수에 타입을 선언하는 것은 비생산적이며 형편없는 스타일
  • 비구조화 할당문은 모든 지역변수의 타입이 추론되도록 하므로
    타입선언을 추가할 경우 코드가 번잡해짐
  • 타입은 일반적으로 처음 등장할 때 결정
  • 이상적인 코드는 함수내에서 생성된 지역변수에 타입을 넣지 않음
    => 방해되는 것들 최소화, 구현로직에 집중
  • 보통 타입정보가 있는 라이브러리에서는 콜백 함수의 매개변수 타입은 자동 추론됨
  • 반환타입을 명시해야 하는이유
    1) 오류의 위치를 제대로 표시
    2) 명명된 타입을 사용하여 더욱 지관적으로 표시
  • 객체 리터럴과 함수 반환에는 타입 명시를 해야 사용자 코드 위치에 오류 나타남

🔍 린터 규칙: no-infferable-types
-> 작성된 모든 타입 구문이 정말로 필요한지 확인

☻ 20 다른 타입에는 다른 변수 사용하기

  • 변수의 값은 바귈 수 있지만 타입은 보통 바귀지 않음
  • 다른 타입에는 별도의 변수를 사용하는 것이 바람직한 이유
    1) 관련 없는 두 값을 분리
    2) 변수명 더 구체적으로 가능
    3) 타입추론 향상, 타입 구문이 불필요해짐
    4) 타입이 더 가결해짐
    5) let 대신 const로 선언하게 되어 타입추론에 도움

☻ 21 타입 넓히기

타입 넓히기

-> 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추

const mixed = ['x', 1];
('x' | 1)[]
['x', 1]
[string, number]
readonly [string, number]
(string|number)[]
readonly (string|number)[]
[any, any]
any[]
  • const로 넓히기 과정을 제어할 수 있음
const x = 'x'; // 타입이 'x'
let vec = {x: 10, y: 20, z:30};
getComponent(vec, x); // 정상
  • 잘못된 추론을 할 정도로 구체적으로 수행하지는 않음
  • 타입스크립트의 기본 동작을 재정의하는 세 가지 방법
    1) 명시적 타입 구문 제공
const v: { x: 1|3|5; } = {
 x: 1, 
}; // 타입이 { x: 1|3|5; }

2) 타입 체커에 추가적 문맥 제공

ex) 함수의 매개변수로 값을 전달`

3) const 단언문 사용

const v1 = {
 x: 1,
 y: 2,
}; // 타입은 { x: number; y: number;}

const v2 = {
 x: 1 as const, // const 단언
 y: 2,
}; // 타입은 { x: 1; y: number;}

const v3 = {
 x: 1, 
 y: 2,
} as const; // const 단언
// 타입은 { readyonly x: 1; readyonly y: 2;}

const a1 = [1,2,3]; // 타입은 number[]
const a2 = [1,2,3] as const; // 타입이 readonly [1,2,3]

☻ 22 타입 좁히기

  • 일반적으로 조건문에서 타입을 좁히는데 능숙
  • 타입을 좁히는 방법 > 명시적 태그를 붙이는 것 (태그된 유니온, 구별된 유니온)
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent  { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent){
  switch (e.type) {
    case 'download':
      e // DownloadEvent
      break; 
    case 'upload':
      e // UploadEvent
      break;
  }
}
  • 사용자 정의 타입 가드
    -> 타입스크립트가 타입을 식별하지 못할 때 커스텀 도입
function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return 'value' in el;
}

function getElementContent(el: HTMLElement){
  if (isInputElement(el)) {
    el; // 타입이 HTMLInputElement
    return el.value;
  }
  el; // 타입이 HTMLElement
  return el.textContent;
}

🤔❔ 타입 가드 예시
NAME is TYPE 형태의 타입 술부(Predicate)를 반환 타입으로 명시

function isNumber(val: string | number): val is number {
  return typeof val === 'number';
}
function someFunc(val: string | number) {
   if (isNumber(val)) {
      val.toFixed(2);
   } else {
      val.split("");
   }
}
someFunc(123);

☻ 23 한꺼번에 객체 생성하기

  • 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리
  • 나눠서 만들어야 할 떄는 단언문을 사용할 수 도 있음
  • 객체 전개 연산자를 사용해 새 변수를 사용하는 것이 나음
  • 조건부 속성 추가시 null 또는 {} 이용하여 객체 전개 사용
declare let hasMiddle: boolean
const firstLast = { first: 'Harry', last: 'Truman' }
const president = { ...firstLast, ...(hasMiddle ? { middle: 'S' } : {}) }

// const president: {
//    middle?: string;
//    first: string;
//    last: string;
// }
  • 선택적 필드 표현시 헬퍼함수 사용 권장
function addOptional<T extends object, U extends object>(a: T, b: U | null): T & Partial<U> {
  return { ...a, ...b }
}

declare let hasMiddle: boolean
const firstLast = { first: 'Harry', last: 'Truman' }
const president = addOptional(firstLast, hasMiddle ? { middle: 'S' } : null)
president.middle // OK, type is string | undefined
  • 객체나 배열 변환하여 새 배열 생성 시
    루프대신 Lodash같은 유틸리티 라이브러리를 사용하는 것 권장

☻ 24 일관성 있는 별칭 사용하기

  • 함수가 타입 정제를 무효화하지 않는다고 가정하지만 실제로 무효화될 가능성 o
    -> 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있다는 점 주의
  • 별칭은 타입스크립트가 타입을 좁히는 것을 방해
  • 비구조화 문법을 사용해 일관된 이름을 사용하는 것이 좋음

☻ 25 비동기 코드에는 콜백 대신 aync 함수 사용하기

  • 콜백보다는 프로미스가 코드를 작성하기 쉽고 타입을 추론하기 쉬움
  • async/awiat을 사용하면
    1) 더 간결하고 직관적인 코드가 됨
    2) 항상 프로미스를 반환하도록 강제됨
    3) 비동기 코드의 개념을 잡는 데 도움이 됨

🤔❔Promise<T>

new Promise<T>(
  ( resolve: (sucessValue: T) => void, 
  	reject: (any) => void
  ) => {
  	//코드 구현
  }
);
// 타입스크립트에서 Promise는 다음처럼 제네릭 클래스 형태로 사용
const numPromise: Promise<number> = new Promise<number>(콜백 함수);
const strPromise: Promise<string> = new Promise<string>(콜백 함수);
const arrayPromise: Promise<number[]> = new Promise<number[]>(콜백 함수);

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

  • 타입을 추론할 때 값이 존재하는 곳의 문맥까지 살핌
type Language = 'JavaScript' | 'TypeScript' | 'Python'
function setLanguage(language: Language) {
  /* ... */
}

setLanguage('JavaScript') // OK

let language = 'JavaScript'
setLanguage(language) // Error. Argument of type 'string' is not assignable to parameter of type 'Language'.
// 해결법 1 - 타입을 선언해 값을 제한하는 것
let language: Language = 'JavaScript'
setLanguage(language) // OK

// 해결법 2 - 상수로 만드는 것
const language = 'JavaScript'
setLanguage(language) // OK
  • 튜플 사용 시 주의점
function panTo(where: [number, number]) {
  /* ... */
}

panTo([10, 20]) // OK

const loc = [10, 20] // number[]
panTo(loc) // Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
// 해결법 1 - 명시적 선언
const loc: [number, number] = [10, 20]
panTo(loc) // OK

// 해결법 2 - as const를 사용해 상수 문맥을 제공: 너무 과하게 정확
const loc = [10, 20] as const // readonly [10, 20]
panTo(loc)
// ~~~ Type 'readonly [10, 20]' is 'readonly'
//     and cannot be assigned to the mutable type '[number, number]'
// 오류는 타입 정의가 아니라 호출되는 곳에서 발생

// 해결법 3 - panTo 함수에 readonly 구문을 추가
function panTo(where: readonly [number, number]) {
  /* ... */
}

const loc = [10, 20] as const
panTo(loc) // OK
  • 객체 사용 시 주의점
type Language = 'JavaScript' | 'TypeScript' | 'Python'
interface GovernedLanguage {
  language: Language
  organization: string
}

function complain(language: GovernedLanguage) {
  /* ... */
}

complain({ language: 'TypeScript', organization: 'Microsoft' }) // OK

const ts = {
  language: 'TypeScript',
  organization: 'Microsoft',
}
complain(ts)
//       ~~ Argument of type '{ language: string; organization: string; }'
//            is not assignable to parameter of type 'GovernedLanguage'
//          Types of property 'language' are incompatible
// 해결법 1 - 타입 선언 추가
const ts: GovernedLanguage = {
  // ... 
}

// 해결법 2 - as const를 사용
const ts = {
  language: 'TypeScript',
  organization: 'Microsoft',
} as const

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

  • 직접 구현하기 보다 내장된 함수형 기법과 서드파트 라이브러리를 사용하는 것 권장
    1) 타입 흐름 개선
    2) 가독성 높임
    3) 명시적 타입 구문 필요성 줄임
    4) 시간 단축
  • 타입스크립트의 많은 부분이 자바스크립트 라이브러리의 동작을 정확히 모델링하기 위해 개발

📖 4장 타입 설계

☻ 28 유효한 상태만 표현하는 타입을 지향하기

  • 유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란을 초래, 오류 유발

☻ 29 사용할 때는 너그럽게, 생성할 때는 엄격하게

"당신의 작업은 엄격하게 하고 다른 사람의 작업은 너그럽게 받아들여야 한다."

  • 사용하기 편리한 API일수록 타입의 범위가 매개변수는 넓고 반환타입은 구체적이며 엄격함

☻ 30 문서에 타입 정보를 쓰지 않기

  • 함수의 입출력의 타입을 코드로 표현하는 것이 주석보다 더 나음
  • 주석은 동기화되지 않지만 타입구문은 타입체커가 타입 정보를 동기화하도록 강제
  • 변수명에 타입 정보를 넣지 않는 것 권장
    ex) ageNum (x) age: number (o)

☻ 31 타입 주변에 null 값 배치하기

  • 한 값의 null 여부가 다른 값의 null 여부에 관련되도록 설계하면 안됨
  • 반환 타입 전체가 null이거나 null이 아니게 만들어야 함
  • 클래스를 만들 때 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록함

☻ 32 유니온 인터페이스 보다는 인터페이스의 유니온 사용하기

  • 어떤 데이터 타입을 태그된 유니온으로 표현하는 것 권장
  • 필드가 관련되어 있다면 두 개의 속성을 하나의 객체로 모으는 것이 더 나은 설계
/* bad */
interface Person {
  name: string;
  // 다음은 둘 다 동시에 있거나 동시에 없습니다
  placeOfBirth?: string;
  dateOfBirth?: Date;
}

/* good */
interface Person {
name: string;
  birth?: {
  	place: string;
  	date: Date;
  }
}

☻ 33 string 타입보다 더 구체적인 타입 사용하기

  • stringany와 비슷한 문제를 가지고 있어
    잘못 사용하면 무효한 값을 허용하고 타입간의 관계도 감추게 됨
  • string타입보다는 문자열 리터럴 타입의 유니온을 사용
  • 객체의 속성 이름을 매개변수로 받을 때는 string보다 keyof T를 사용

☻ 34 부정확한 타입보다는 미완성 타입을 사용하기

  • 타입이 없는 것보다 잘못된게 더 나쁨
  • 타입이 구체적으로 정제된다고 해서 무조건 정확도가 올라가지 않음

🤔❔ any vs unknown

let notSure: unknown;
notSure = "maybe a string instead";
// any type에는 unknown 타입 할당 가능
let anyType: any;
anyType = notSure;
// any, unknown 이외의 type에는 unknown 타입 할당 불가능
let numberType: number;
numberType = notSure; // error
  • 공통점: 모든 타입을 허용
  • 차이점
anyunknown
"어떤 타입이든 가능하다""어떤 타입인지 알 수 없다, 모른다"
타입 검사를 느슨하게하여 체크 하지 않음프로퍼티 또는 연산을 하는 경우 컴파일러가 체크
타입을 좁혀서 사용하지 않아도 되서 자유로움컴파일 에러가 나므로 무조건 타입을 좁혀서 사용(타입 가드 필요)

-> any보다는 unknown을 사용하는것이 좀 더 안전하게 코딩을 할 수 있는 방법

☻ 35 데이터가 아닌, API와 명세를 보고 타입 만들기

  • GraphQL의 장점은 특정 쿼리에 대해 타입스크립트 타입 생성
  • 데이터에 드러나지 않는 예외적인 경우들이 문제가 될 수 있기 때문에
    데이터보다 명세로부터 코드를 생성하는 것 권장

☻ 36 해당 분야의 용어로 타입 이름 짓기

"컴퓨터 과학에서 어려운일은 캐시 무효화이름 짓기 뿐이다"

  • 자체적으로 용어를 만들지말고 해당 분야에 이미지 존재하는 용어 사용
  • 동일한 의미는 같은 용어를 사용
  • 모호하고 의미없는 이름 사용 지양
  • 포함한 내용이나 계산 방식이 아닌 데이터 자체가 무엇인지 고려해야 함
  • 좋은 이름은 추상화 수준을 높이고 의도치 않은 충돌 위험성 줄여 줌

☻ 37 공식 명칭에는 상표를 붙이기

  • 구조적 타이핑을 사용하기 때문에 값을 세밀하게 구분 못하는 경우 존재
interface Vector2D {
  x: number;
  y: number;
}
function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y);
}

const vec3D = { x: 3, y: 4, z: 1 };
calculateNorm(vec3D);
  • 값의 구분을 위해 공식 명칭이 필요하다면 상표를 부이는 것을 고려
interface Vector2D {
  _brand: "2d";
  x: number;
  y: number;
}
function vec2D(x: number, y: number): Vector2D {
  return { x, y, _brand: "2d" };
}
function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y);
}

calculateNorm(vec2D(3, 4)); // 정상
const vec3D = { x: 3, y: 4, z: 1 };
calculateNorm(vec3D); // '_brand' 속성이 없습니다.
  • 상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과

📖 5장 any 다루기

  • 타입시스템은 선택적이고 점진적이어서 정적이면서도 동적인 특성
  • 마이그레이션할 때 any 타입이 중요한 역할

☻ 38 any 타입은 가능한 좁은 범위에서만 사용하기

  • 다른 코드에 영향이 가지않는 방향으로 최소한의 범위에만 작성
// bad
function f1() {
  const x: any = expressionReturningFoo();
  processBar(x);
}
// good
function f2() {
  const x = expressionReturningFoo();
  processBar(x as any);
}
  • any 타입을 반환하면 타입 안정성이 나빠짐
  • 강제 타입 오류 제거시 any대신 @ts-ignore 사용 권장

☻ 39 any를 구체적으로 변형해서 사용하기

  • any 타입의 값을 그대로 정규식이나 함수에 넣지 말아야 함
// bad
function getLengthBad(array: any) {
  return array.length;
}
// good
function getLength(array: any[]) {
  return array.length;
}
// 객체이긴 하지만 값을 알 수 없는 경우
function paramTest({[key: string]: any}) {
  ...
}

☻ 40 함수 안으로 타입 단언문 감추기

  • 함수 내부에서는 타입 단언을 사용하고 외부로 드러나는 부분만 타입 정의를 명확히 명시
  • 불가피하게 필요한경우 any 단언문은 타입이 정의된 함수 안으로 감추는 것이 더 좋은 설계

☻ 41 any의 진화를 이해하기

const result = []; // any[]
result.push('a'); // string []
result.push(1); // (string|number) []
  • noImplicity가 설정된 상태에서 변수의 타입이 암시적인 any에 값을 할당할 경우에만 진화
  • 명시적 any 선언시 타입 그대로 유지
  • 어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류 발생
  • 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않음
function makeSquares(start: number, limit: number) {
  const out = []; // 'out' 변수는 일부 위치에서 암시적으로 'any[]' 형식입니다.
  range(start, limit).forEach((i) => {
    out.push(i * i);
  });
  return out; // 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
}
  • 타입을 안전하게 지키기 위해 명시적 타입 구문을 사용하는 것 권장

☻ 42 모르는 타입의 값에는 any대신 unknown을 사용하기

any vs unknown

  • any / unknown / never
anyunknownnever
모든 타입 any 타입에 할당가능모든 타입 unknown 타입에 할당가능어떤 타입도 never에 할당 불가능
어떠한 타입으로도 할당 가능unknown과 any타입에만 할당 가능어떠한 타입으로도 할당 가능
  • 제네릭보다 unknown을 반환하고 단언문을 사용하거나 원하는대로 타입을 좁히는 것이 좋음
  • object 또는 {} 를 사용하는 방법은 unknown보다는 범위가 좁음
  • object 타입은 모든 비기본형 타입으로 객체와 배열 포함
  • {} 타입은 nullundefined 제외한 모든 값 포함
  • nullundefined가 불가능하다고 판단되는 경우 unknown 대신 {}사용

☻ 43 몽키 패치보다는 안전한 타입을 사용하기

  • 모듈 관점에서 제대로 동작하게 하려면 global 선언 추가 필요
export {};
declare global {
  interface Document {
    monkey: string;
  }
}
  • 구체적인 타입 단언문 사용 권장
interface MonkeyDocument extends Document {
  monkey: string;
}
(document as MonkeyDocument).monkey = "Monkey";
  • 몽키패치를 남용해서는 안되며 궁극적으로 더 잘 설계된 구조로 리팩토링 권장

☻ 44 타입 커버리지를 추적하여 타입 안전성 유지하기

  • 프로젝트에서 any개수를 추적하는 방법
// 백분율 출력
$ npx type-coverage
// any타입 위치 모두 출력
$ npx type-coverage --detail

📖 6장 타입 선언과 @types

☻ 45 devDependencies에 typescript와 @types 추가하기

전이 의존성
: 다른 사용자가 프로젝트 설치 시 dependencies에 들어 있는 라이브러리도 설치 됨

peerDependenceise
: 런타임에 필요하긴 하지만 의존성을 직접 관리하지 않는 라이브러리들

DefinitelyTyped git
: 타입스크립트 커뮤니티에서 유지보수하고 있는 자바스크립트 라이브러리의 타입을 정의한 모음

  • @types의 의존성은 devDependencies에 있어야 함

☻ 46 타입 선언과 관련된 세 가지 버전 이해하기

다음 세가지 버전 중 하나라도 맞지 않으면 오류 발생
1) 라이브러리의 버전
2) 타입선언(@types)의 버전
3) 타입스크립트의 버전

  • 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점
    1) 라이브러리는 업데이트 && 타입 선언은 업데이트 하지 않은 경우
    2) 라이브러리보다 타입 선언의 버전이 최신
    3) 타입스크립트 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신
    4) @types 의존성 중복

  • 번들링 방식의 문제점
    1) 타입스크립트 버전이 올라가며 오류 발생(번들된 타입에서는 @types 버전 선택 불가능)
    2) 프로젝트 내 타입선언이 다른 라이브러리의 타입 선언에 의존하는 경우
    3) 과거 버전에 있는 타입 선언에 문제가 있는 경우 과거 버전에서 패치 업데이트 해야 함
    4) 타입 선언의 패치 업데이트를 자주 하기 어려움

🎯 결론: DefinitelyTyped를 사용하자
=> 라이브러리 업데이트 & 해당 @types 업데이트

☻ 47 공개 API에 등장하는 모든 타입을 익스포트하기

  • 라이브러리 사용자를 위해 명시적으로 모든 타입을 익스포트 하는 것이 좋음
  • ParametersReturnType 제너릭 타입을 사용하여 추출할 수 있긴 함

☻ 48 API 주석에 TSDoc 사용하기

JSDoc vs TsDoc
1) TSDoc 주석은 마크다운 형식으로 꾸며짐 (in @example)
2) 타입정보가 코드에 있기때문에 @param@returns에 타입 정보를 명시하지 않음
3) 에러타입 역시 명시하지 않고 @link 등으로 API문서와 연결하기도 함
4) TSDoc 설명은 한줄(80자 미만)이어야 하며 부가적 설명이 필요한 경우 @remarks이용

☻ 49 콜백에서 this에 대한 타입 제공하기

  • this는 다이나믹 스코프로 정의된 방식이 아닌 호출된 방식에 따라 달라짐
  • 자바스크립트에서는 함수 인자의 이름으로 this를 허용하지 않음
  • 타입스크립트에서는 이를 이용하여 첫 번째 인자의 이름이 this인 경우 이를 함수의 this에 특정한 타입을 바인딩해서 호출해야 한다는 의미로 사용
function addKeyListener (
  el: HTMLElement,
  fn: (this: HTMLElement, e:KeyboardEvent) => void
) {
  el.addEventListener('keydown', e => {
    fn.call(el, e)
    fn(el, e); // 1개의 인수가 필요한데 2개를 가져옴
    fn(e); // 'void' 형식의 'this' 컨텍스트를 'HTMLElement' 형식에 할당 할 수 없음
  })
}
  • 콜백 함수에서 this를 사용해야 한다면, 타입 정보를 명시해야 함

☻ 50 오버로딩 타입보다는 조건부 타입을 사용하기

  • 오버로딩 타입보다 조건부 타입을 사용하는 것이 좋음
  • 조건부 타입은 추가적인 오버로딩 없이 유니온 타입 지원 가능
function double<T extends string | number>(x: T): T extends string ? string : number;

☻ 51 의존성 분리를 위해 미러 타입을 사용하기

미러링(mirroring)
: 의존하는 라이브러리 구현과 무관하게 타입에만 의존할 때 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것,
=> 의존성 분리에 유용, 프로젝트가 커지면 적용이 어렵고 명시적으로 의존성을 추가 하는 것 권장

☻ 52 테스팅 타입의 함정에 주의하기

  • 타입을 테스트 할 때 함수 타입의 동일성할당 가능성 차이점 주의
  • dtslint 사용 권장 (DefinitelyTyped의 타입 선언을 위한 도구)
// 특별한 형태의 주석을 통해 동작
const beatles = ['john', 'paul', 'george', 'ringo'];
map(beatles, function(
  name, // $ExpectType string
  i, // $ExpectType number
  array // $ExpectType string[]
) {
  this // $ExpectType string[]
  return name.length;
}) // $ExpectType number[]
/* 글자 자체를 비교한다는 단점이 존재 */
  • 함수를 실행만 하기 보다 타입을 체크하는 것이 훨씬 좋은 테스트 코드
  • 콜백 매개변수의 추론된 타입을 체크, this 역시 테스트 권장

블랙박스 스타일
: 중간 단계의 세부 사항은 테스트 하지 않음


📖 7장 코드를 작성하고 실행하기

☻ 53 타입스크립트 기능보다는 ECMAScript 기능 사용하기

  • 타입스크립트의 역할을 명확히 하기위해 다음은 사용하지 않는 것이 좋음
    열거형(enum)
    매개변수 속성
    트리플 슬래시 임포트
    데코레이터
    => 타입정보를 제거한다고 자바스크립트가 되지 않음

  • 열거형(enum)의 문제점
    1) 숫자형에 0, 1, 2 외의 가른 숫자가 할당되면 위험
    2) 대체제인 상수형 열거형은 런타임에 완전히 제거됨
    (문자열 열거형과 다른 동작 / 바벨로 트랜스파일 될 수 없음)
    3) 명목적 타이핑(타입의 이름이 같아야 할당 허용 !== 구조적타이핑) 사용

    const enum Flavor {
      VANILLA = "vanilla",
      CHOCOLATE = "chocolate",
      STRAWBERRY = "strawberry",
    }
    
    let flavor = Flavor.CHOCOLATE;
    flavor = "strawberry";
    // Type '"strawberry"' is not assignable to type 'Flavor'.
    
    import { Flavor } from "ice-cream";
    scoop(Flavor.VANILLA);

    4) 열거형대신 리터럴 타입의 유니온 사용 권장

    type Flavor = "vanilla" | "chocolate" | "strawberry";
     let Flavor: Flavor = "chocolate"; // 자바스크립트와 호환되고 자동완성 기능도 사용 가능

    5) 특정 번들러에서 TreeShaking 안됨

🤔 ❔ 열거형대신 리터럴 타입의 유니온 사용 권장

  • Flavor 개수가 많다면..?
  • scoop(Flavor.CHOCOLATE) vs scoop("chocolate") ?
  • enum을 사용하지 않고 상수를 const object를 통해서 관리할 때 type | interface정의 할 경우, 정의된 상수를 사용 못함
const Flavor = {
	VANILLA = "vanilla",
    CHOCOLATE = "chocolate",
  	STRAWBERRY = "strawberry",
};
type TypeFoo = {
	flavor: Flavor.VANILLA;
	// ERROR: Cannot find namespace 'Flavor'
}

❗️

export const Flavor = {
	VANILLA: "vanilla",
    CHOCOLATE: "chocolate",
    STRAWBERRY: "strawberry",
} as const; // as const 를 붙여야 "vanilla" | "chocolate" | "strawberry"
type Flavor = typeof Flavor[keyof typeof Flavor];
// 붙이지 않을 경우 string
  • 매개변수 속성의 문제점
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
// 매개변수 속성 - public name
class Person {
  constructor(public name: string) {}
}

1) 더 간결한 문법을 제공하지만 컴파일 시 코드 늘어남
2) 일반 속성과 섞어 사용할 시 클래스 설계 혼란
3) 이질적이고 초보자에게 생소한 문법

☻ 54 객체를 순회하는 노하우

  • 객체를 다룰 때 프로토타입 오염 가능성 염두
  • 상수이거나 추가적인 키 없이 정확한 타입을 원하는 경우 keyof 선언이 적절
  • 키와 값의 타입을 다루기 까다롭지만 타입 문제 없이
    단지 객체의 키와 값을 순회하고 싶은 경우 일반적으로 Object.entires 쓰임

☻ 55 DOM 계층 구조 이해하기

🔍 참고 자료
lib.dom.d.ts

  • DOM 계층의 타입들
타입예시
EventTargetwindow, XMLHttpRequest
Nodedocument, Text, Comment
ElementHtmlElement, SVGElemnet
HTMLElement<i>, <b>
HTMLButtonElement<button>
  • DOM 타입과 관련해서는 단언문 사용 권장

❗️ 인라인 함수
🔍 자바스크립트 인라인함수 link1 link2
: 함수 정의를 변수에 할당

const test = function() {
	console.log("이것은 인라인 함수입니다.");
}

일반 함수의 경우는 브라우저 자바 엔진이 javascript 를 코드 전체를 파싱(Parsing)하는 단계에 생성되는 반면, 인라인 함수는 변수에 대입되는 방식이므로 런타임(Runtime) 시에 생성

❓ 🤔 277pg "인라인 함수와 이벤트 타입 변경 적용하여 오류제거"
인라인 함수 어디...?

function addDragHandler(el: HTMLElement) {
  el.addEventListener('mousedown', eDown => {
    const dragStart = [eDown.clientX, eDown.clientY];
    const handleUp = (eUp: MouseEvent) => {
      el.classList.remove('dragging');
      el.removeEventListener('mouseup', handleUp);
      const dragEnd = [eUp.clientX, eUp.clientY];
      console.log('dx, dy = ', [0, 1].map(i => dragEnd[i] - dragStart[i]));
    }
    el.addEventListener('mouseup', handleUp);
  });
}
const div = document.getElementById('surface');
if (div) {
  addDragHandler(div);
}

❗️🔍 인라인함수
: 일반적인 함수의 호출 과정을 거치지 않고, 함수의 모든 코드를 호출된 자리에 바로 삽입하는 방식의 함수

function handleDrag(eDown: Event) {
  const targetEl = eDown.currentTarget;
  targetEl.classList.add('dragging');
// ~~~~~~~           Object is possibly 'null'.
//         ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
  const dragStart = [
     eDown.clientX, eDown.clientY];
        // ~~~~~~~                Property 'clientX' does not exist on 'Event'
        //                ~~~~~~~ Property 'clientY' does not exist on 'Event'
  const handleUp = (eUp: Event) => {
    targetEl.classList.remove('dragging');
//  ~~~~~~~~           Object is possibly 'null'.
//           ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
    targetEl.removeEventListener('mouseup', handleUp);
//  ~~~~~~~~ Object is possibly 'null'
    const dragEnd = [
       eUp.clientX, eUp.clientY];
        // ~~~~~~~                Property 'clientX' does not exist on 'Event'
        //              ~~~~~~~   Property 'clientY' does not exist on 'Event'
    console.log('dx, dy = ', [0, 1].map(i => dragEnd[i] - dragStart[i]));
  }
  targetEl.addEventListener('mouseup', handleUp);
// ~~~~~~~ Object is possibly 'null'
}
   const div = document.getElementById('surface');
   div.addEventListener('mousedown', handleDrag);
// ~~~ Object is possibly 'null'
  • Event의 더 구체적인 타입들
타입설명
UIEvent모든 종류의 사용자 인터페이스 이벤트
MouseEvent클릭처럼 마우스로부터 발생되는 이벤트
TouchEvent모바일 기기의 터치 이벤트
WheelEvent스크롤 휠을 돌려서 발생되는 이벤트
KeyboardEvent키 누름 이벤트
  • DOM 엘리먼트와 이벤트에는 충분히 구체적인 타입 정보를 사용하거나,
    타입스크립트가 추론할 수 있도록 문맥 정보를 활용해야 함

☻ 56 정보를 감추는 목적으로 private 사용하지 않기

  • 타입스트립트의 접근 제어자들은 단지 컴파일 시점에만 오류를 표시(런타임 효력x)

정보를 숨기기 위해 가장 효과적인 방법은 클로저 사용

  • 클로저 방식은 동일 클래스 개별 인스턴스간 속성접근 불가능

접두사로 #을 붙여 비공개 필드기능 사용 가능 (현재 표준화 진행중)

class ClassWithPrivateMethod {
  #privateMethod() {
    return 'hello world';
  }
  getPrivateMessage() {
    return this.#privateMethod();
  }
}
const instance = new ClassWithPrivateMethod();
console.log(instance.getPrivateMessage()); // 'hello world'
ClassWithPrivateMethod.#privateMethod = 'good' // Private field '#privateMethod' must be declared in an enclosing class
  • 동일한 클래스의 다른 인스턴스가 서로의 비공개 멤버를 읽을 수 있습니다.
class Thing {
  static #counter = 0;
  #hidden;
  constructor() {
    this.someProperty = 'foo';
    this.#hidden = ++Thing.#counter;
  }
  showPublic() {
    return this.someProperty;
  }
  showPrivate() {
    return this.#hidden;
  }
}
const thing = new Thing();
const thing2 = new Thing();
const thing3 = new Thing();
console.log(thing3.showPrivate()) // 3
  • 비공개 필드를 지원하지 않는 자바스크립트 버전으로 컴파일 진행시WeakMap으로 대체

❔ 🤔 WeakMap ?
키에 대한 강력한 참조를 생성하지 않는 키/값 쌍의 모음으로
키는 반드시 객체이며, 값은 임의의 JavaScript 타입

  • 클로저와 비교하여 생성자에서 생성된 모든 인스턴스에 대해 동일한 WeakMap을 재사용가능
    => 메모리 효율성이 더 높음
let Thing;
{
  const privateScope = new WeakMap();
  let counter = 0;
  Thing = function() {
    this.someProperty = 'foo';
    privateScope.set(this, {
      hidden: ++counter,
    });
  };
  Thing.prototype.showPublic = function() {
    return this.someProperty;
  };
  Thing.prototype.showPrivate = function() {
    return privateScope.get(this).hidden;
  };
}
console.log(typeof privateScope);
// "undefined"
const thing = new Thing();
console.log(thing);
// object {someProperty : "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
console.log(thing.privateScope.hidden);
//  Cannot set properties of undefined
thing.privateScope.hidden = 72
//  Cannot set properties of undefined
class Thing {
  static #counter = 0;
  #hidden;
  constructor() {
    this.someProperty = 'foo';
    this.#hidden = ++Thing.#counter;
  }
  showPublic() {
    return this.someProperty;
  }
  showPrivate() {
    return this.#hidden;
  }
}
const thing = new Thing();
console.log(thing);
// object {someProperty : "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
  • WeakMap을 쓰는 이유는 WeakMap의 키는 기존 객체를 약한 참조해서 가비지컬렉션을 방해하지 않기 때문
    - 약한 참조: 할당된 객체를 해지하기 위해 null을 넣으면 GC 수집대상이 되고 객체는 사라짐
    - 강한 참조: 할당된 객체를 해지하기 위해 null을 넣어도 GC가 수집해가지 않음
  • 객체 안에 속성을 추가하기 싫고 객체에 정보를 저장하고 싶을 때 사용
let sally = { name: 'sally'};
let map = new Map();
map.set(sally, "hi")
sally = null; 
// sally is stored inside the map,
// we can get it by using map.keys()
let weakSally = { name: 'weakSally'};
let map = new Map();
map.set(sally, "hi")
weakSally = null; 
// weakSally is removed from memory

👀 활용 예시

☻ 57 소스맵을 사용하여 타입스크립트 디버깅하기

  • 디버거 좌측의 파일목록에서 기울임 글꼴은 웹 페이지에 포함된 '실제'파일이 아니라는 뜻

📖 8장 타입스크립트로 마이그레이션하기

🎯 타입스크립트 사용을 위한 설득 근거

  • 2017의 한 조사에 따르면
    자바스크립트 프로젝트의 15%는 타입스크립트였다면 컴파일 시점에 방지 가능
  • 에어비엔비의 사후 분석 6개월치 조사 결과
    발견 버그의 38%가 타입스크립트에서 방지 가능한 것들

☻ 58 모던 자바스크립트로 작성하기

  • 타입스크립트를 도입할 때 가장 중요한 기능은 Es6모듈과 ES2015 클래스

  • for(;;) 대신 for-of 또는 배열 메서드 사용
    index 필요한 경우 forEach 사용

    for-in 의 문제점: 📚 느림
    👀 why js for in loops are bad
    => 🤔 주의할 점만 알면 필요할 때 사용하는것에는 문제가 없을 것같음

  • 함수 표현식보다 화살표 함수 사용하기

  • noImplicitThis 설정 시 this 바인딩 관련 오류 표시

    📌 this 스코프 정리
    1) 전역공간: 전역객체(window/global)를 참조
    2) 메서드로서 호출한경우 (obj.func(), obj[]() - 앞에 객체가 명시 되어 있는 경우)
    :메서드 호출 주체를 참조(obj)
    3) 함수로서 호출한 경우: 전역객체를 참조 (메서드 내부함수에서도 동일)
    4) 콜백함수 내부에서의 this:
    제어권을 넘겨받은 함수가 정의한 바에 따르며
    그렇지 않은경우 전역객체 참조
    개발자가 this를 바인딩해서 넘기면 그에 따름
    5) 생성자 함수에서의 this: 생성될 인스턴스 참조

  • 단축 객체 표현과 구조 분해 할당 사용하기

const props = obj.props;
const a = props.a;
const b = props.b;
// 아래와 같이 줄이기 가능
const {props: {a, b}} = obj; // a,b는 변수지만 props는 변수선언이 아님을 주의
// 구조 분해 문법 내에서 기본값 지정 가능
const {a = 'default'} = obj.props
// 배열 구조분해 시 첫 요소 무시
const [, a, b] = [x, y, z]
// 매개변수에서도 사용 가능
point.forEach(([x, y, z]) => console.log(x + y + z));

☻ 59 타입스크립트 도입 전에 @ts-check와 JSDoc으로 시험해 보기

  • @ts-check로 간단히 타입 체크 동작 실험 가능하며 오류를 빠르게 잡아낼 수 있음
  • JSDoc 주석은 중간 단계이기 때문에 공들일 필요 없음.
    최종 목표는 타입스크립트로된 .ts 파일

☻ 60 allowJs로 타입스크립트와 자바스크립트 같이 사용하기

  • 타입스크립트와 자바스크립트 공존의 핵심은 allowJs 컴파일러 옵션
  • 기존 빌드과정에 타입스크립트 컴파일러 추가와 모듈 단위로 전환하는 과정에서
    allowJs 옵션 필요
  • 대규모 마이그레이션 작업 시작 전, 테스트와 빌드 체인에 타입스크립트 적용 권장

☻ 61 의존성 관계에 따라 모듈 단위로 전환하기

  • 점진적 마이그레이션 진행 시 모듈 단위로 각개격파하는 것이 이상적
  • 의존성 관계 시각화 한 후 다른 모듈에 의존하지 않는 최하단 모듈(보통 util)부터 시작
  • 서드파트 라이브러리 타입 정보 @types 모듈 설치로 가장 먼저 해결 권장
  • 외부 API 먼저 해결 권장
  • 마이그레이션 진행 시 리팩터링은 목록만 만들어두고 진행하면 안됨
  • 테스트 코드는 의존성 관계도의 최상단에 위치하며 마지막 단계로 진행

☻ 62 마이그레이션의 완성을 위해 noImplicitAny 설정하기

  • noImplicitAny설정으로 마이그레이션의 마지막 단계 진행
  • noImplicitAny를 로컬에만 설정하고 작업하는 것이 좋음
    1) 빌드 실패 방지
    2) 점진적 마이그레이션 가능
    3) 나타나는 오류 개수로 진척도를 나타내는 지표로 활용
  • 타입체크의 강도 stirct: true > noImplicitAny > strictNullChecks

profile
Junior Web FE Developer

0개의 댓글