[TypeScript 독학] #9 이펙티브 타입스크립트(3)

안광의·2022년 4월 10일
0

TypeScript 독학

목록 보기
9/12
post-thumbnail

시작하며

지난글에 이어서 이펙티브 타입스크립트의 5,6장의 내용을 정리하였다. 이번 챕터에는 any에 대한 내용과 타입스크립트가 어떻게 타입을 추론하는지에 대한 내용이 담겨 있고 이를 적절하게 활용하기 위해서 어떻게 코드를 작성해야 하는지에 대한 내용이 포함되어 있다.



5장 any 다루기

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

interface Foo { foo: string; }
interface Bar { bar: string; }
declare function expressionReturningFoo(): Foo;
function processBar(b: Bar) { /* ... */ }
function f1() {
  const x: any = expressionReturningFoo();  // Don't do this
  processBar(x);
}

function f2() {
  const x = expressionReturningFoo();
  processBar(x as any);  // Prefer this
}

//any를 사용하려면 두번째 방법이 함수 호출 이후에도 x의 타입이 Foo로 유지되므로 더 나음
function f1() {
  const x = expressionReturningFoo();
  // @ts-ignore
  processBar(x);
  return x;
}

//ts-ignore를 사용해도 타입 오류를 제거할 수 있지만 임시적인 방법은 다른 부분에서 문제가 발생할 수 있기 때문에 근본적인 해결 필요
interface Config {
  a: number;
  b: number;
  c: {
    key: Foo;
  };
}
declare const value: Bar;
const config: Config = {
  a: 1,
  b: 2,  // These properties are still checked
  c: {
    key: value as any
  }
};

//객체 내에서도 any를 최소한으로 사용해서 다른 속성은 타입을 유지하도록 사용

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

function getLengthBad(array: any) {  // Don't do this!
  return array.length;
}

function getLength(array: any[]) {
  return array.length;
}

//any를 사용한다면 최대한 범위를 좁혀서 사용해야 타입체크가 됨
function hasTwelveLetterKey(o: {[key: string]: any}) {
  for (const key in o) {
    if (key.length === 12) {
      return true;
    }
  }
  return false;
}

//객체 내에서도 값에만 any를 사용
type Fn0 = () => any;  // any function callable with no params
type Fn1 = (arg: any) => any;  // With one param
type FnN = (...args: any[]) => any;  // With any number of params
                                     // same as "Function" type

//함수에 any를 사용할 때에도 최대한 구체화하여 사용

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

declare function shallowEqual(a: any, b: any): boolean;
function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[]|null = null;
  let lastResult: any;
  return function(...args: any[]) {
      // ~~~~~~~~~~~~~~~~~~~~~~~~~~
      //          Type '(...args: any[]) => any' is not assignable to type 'T'
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  };
}

//T와 리턴되는 함수가 관련되어 있지는 알지못해 에러 발생
function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[]|null = null;
  let lastResult: any;
  return function(...args: any[]) {
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  } as unknown as T;
}

//단언문을 사용해서 에러 해결
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== b[k]) {
                           // ~~~~ Element implicitly has an 'any' type
                           //      because type '{}' has no index signature
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

//같은 값을 가진 객체인지 검사하는 shallowObjectEqual로 b[k]가 존재한다고 명시되어 있지 않아 에러가 발생함
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== (b as any)[k]) {
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

//b[k]를 (b as any)[k]로 명시하여 에러 해결

any의 진화를 이해하기

function range(start: number, limit: number) {
  const out = [];
  for (let i = start; i < limit; i++) {
    out.push(i);
  }
  return out;  // Return type inferred as number[]
}
//number[]로 추론됨 (noImplicitAny : true 일때)
let val;  // Type is any
if (Math.random() < 0.5) {
  val = /hello/;
  val  // Type is RegExp
} else {
  val = 12;
  val  // Type is number
}
val  // Type is number | RegExp

//처음에 any로 추론된 타입이 값에 따라 좁혀짐
function somethingDangerous() {}
let val = null;  // Type is any
try {
  somethingDangerous();
  val = 12;
  val  // Type is number
} catch (e) {
  console.warn('alas!');
}
val  // Type is number | null

//초깃값이 null인 경우에도 any 타입으로 추론됨
let val: any;  // Type is any
if (Math.random() < 0.5) {
  val = /hello/;
  val  // Type is any
} else {
  val = 12;
  val  // Type is any
}
val  // Type is any

//명시적으로 any 타입을 지정한 경우에는 유지됨
function range(start: number, limit: number) {
  const out = [];  // Type is any[]
  for (let i = start; i < limit; i++) {
    out.push(i);  // Type of out is any[]
  }
  return out;  // Type is number[]
}
function makeSquares(start: number, limit: number) {
  const out = [];
     // ~~~ Variable 'out' implicitly has type 'any[]' in some locations
  range(start, limit).forEach(i => {
    out.push(i * i);
  });
  return out;
      // ~~~ Variable 'out' implicitly has an 'any[]' type
}

//forEach 구문 내에서는 타입 진화가 일어나지 않음
//타입 진화보다는 명시적인 타입 구문을 사용하는 것이 좋음

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

function parseYAML(yaml: string): any {
  // ...
}
interface Book {
  name: string;
  author: string;
}
const book: Book = parseYAML(`
  name: Wuthering Heights
  author: Emily Brontë
`);

//any타입보다는 호출되는 시점에 타입을 지정하여 사용하는 것이 좋음
const book = parseYAML(`
  name: Jane Eyre
  author: Charlotte Brontë
`);
alert(book.title);  // No error, alerts "undefined" at runtime
book('read');  // No error, throws "TypeError: book is not a
               // function" at runtime

//any타입으로 추론될 경우 해당 변수를 사용할 때 에러 발생
function safeParseYAML(yaml: string): unknown {
  return parseYAML(yaml);
}
const book = safeParseYAML(`
  name: The Tenant of Wildfell Hall
  author: Anne Brontë
`);
alert(book.title);
   // ~~~~ Object is of type 'unknown'
book("read");
// ~~~~~~~~~~ Object is of type 'unknown'

//어떤 타입이든 unknown에 할당 가능하지만 반대는 any,unknown만 가능
//never에는 어떤 타입도 할당할 수 없지만 어떤 타입에도 never 할당 가능

//위 두가지 속성을 합친 것이 any(어떤 타입도 할당할 수 있고 어떤 타입에도 할당 가능)
function safeParseYAML(yaml: string): unknown {
  return parseYAML(yaml);
}
interface Geometry {}
function processValue(val: unknown) {
  if (val instanceof Date) {
    val  // Type is Date
  }
}

//값을 모르는 경우도 unknown을 사용하면 추후에 타입을 할당할 수 있음
function isBook(val: unknown): val is Book {
  return (
      typeof(val) === 'object' && val !== null &&
      'name' in val && 'author' in val
  );
}
function processValue(val: unknown) {
  if (isBook(val)) {
    val;  // Type is Book
  }
}

//unknown의 타입을 좁히기 위해서 사용자 정의 타입가드를 사용함
interface Foo { foo: string }
interface Bar { bar: string }
declare const foo: Foo;
let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;


//기능적으로는 둘다 동일하지만 단언문을 분리하는 경우에 바로 에러를 발생시키는 as unknown을 사용하는 것이 바람직함

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

document.monkey = 'Tamarin';
      // ~~~~~~ Property 'monkey' does not exist on type 'Document'

//dom이나 전역변수에 임의로 추가한 속성을 타입스크립트는 알지 못함
(document as any).monkey = 'Tamarin';  // OK


//전역변수의 속성을 추가하는 경우 any로 단언하여 접근
(document as any).monky = 'Tamarin';  // Also OK, misspelled
(document as any).monkey = /Tamarin/;  // Also OK, wrong type

//타입체커가 작동하지 않는 문제점 발생
export {};
declare global {
  interface Document {
    /** Genus or species of monkey patch */
    monkey: string;
  }
}
document.monkey = 'Tamarin';  // OK

//보강을 사용하여 전역변수의 속성에 타입을 부여하는 것이 좋음
interface MonkeyDocument extends Document {
  /** Genus or species of monkey patch */
  monkey: string;
}

(document as MonkeyDocument).monkey = 'Macaque';


//Document의 타입을 확장하여 기존 타입을 건드리지 않음

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

const utils = {
  buildColumnInfo(s: any, name: string): any {},
};
declare let appState: { dataSchema: unknown };

//noImplictAny를 설정하였다고 해도 any 타입을 명시하는 경우나 서드파티 타입으로 인해 영향을 받을 수 있음
//npm type-coverage를 통해 any의 개수를 추적할 수 있음
const utils = {
  buildColumnInfo(s: any, name: string): any {},
};
declare let appState: { dataSchema: unknown };
function getColumnInfo(name: string): any {
  return utils.buildColumnInfo(appState.dataSchema, name);  // Returns any
}
const utils = {
  buildColumnInfo(s: any, name: string): any {},
};
declare let appState: { dataSchema: unknown };
declare module 'my-module';

//모듈을 사용한 경우 타입이 any로 지정되기 때문에 주의해야함


6장 타입 선언과 @types

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

declare function makeButton(props: {text: string, onClick: () => void }): void;
class ResetButton {
  constructor() {
    this.onClick = this.onClick.bind(this);
  }
  render() {
    return makeButton({text: 'Reset', onClick: this.onClick});
  }
  onClick() {
    alert(`Reset ${this}`);
  }
}

//onClick 메서드의 this가 인스턴스 자체를 가리키도록 constructor 내에서 바인딩

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

function double(x: number|string): number|string;
function double(x: any) { return x + x; }
const num = double(12);  // string | number
const str = double('x');  // string | number

//리턴 값이 number | string 타입으로 잡힘
function double<T extends number|string>(x: T): T;
function double(x: any) { return x + x; }

const num = double(12);  // Type is 12
const str = double('x');  // Type is "x"

//리턴 값의 타입이 12, 'x'로 너무 좁혀짐
function double(x: number): number;
function double(x: string): string;
function double(x: any) { return x + x; }

const num = double(12);  // Type is number
const str = double('x');  // Type is string

//오버로딩하여 number나 string으로 타입이 지정되게 함
function double(x: number): number;
function double(x: string): string;
function double(x: any) { return x + x; }

const num = double(12);  // Type is number
const str = double('x');  // Type is string
function f(x: number|string) {
  return double(x);
             // ~ Argument of type 'string | number' is not assignable
             //   to parameter of type 'string'
}

//유니온 타입은 할당할 수 없는 에러가 발생
function double<T extends number | string>(
  x: T
): T extends string ? string : number;
function double(x: any) { return x + x; }
const num = double(12);  // number
const str = double('x');  // string

// function f(x: string | number): string | number
function f(x: number|string) {
  return double(x);
}

//삼항연자로 조건부타입을 설정하면 유니온 타입도 할당 가능함

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

declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
map(['2017', '2018', '2019'], v => Number(v));

//매개변수의 타입에 대한 오류를 인지하지만 리턴값에 대한 체크는 이루어지지 않아 완전한 테스트라고 할 수 없음
const square = (x: number) => x * x;
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
const lengths: number[] = map(['john', 'paul'], name => name.length);

//불필요한 타입선언이지만 테스트에서는 타입 검사를 할 수 있음.
const square = (x: number) => x * x;
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
function assertType<T>(x: T) {}

assertType<number[]>(map(['john', 'paul'], name => name.length));


//불필요한 변수 대신에 헬퍼함수를 생성하여 테스트
const square = (x: number) => x * x;
declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
function assertType<T>(x: T) {}
const beatles = ['john', 'paul', 'george', 'ringo'];
assertType<{name: string}[]>(
  map(beatles, name => ({
    name,
    inYellowSubmarine: name === 'ringo'
  })));  // OK

//inYellowSubmarine 속성이 추가되어 있는데도 테스트가 통과함
//타입스크립트 함수는 매개변수가 더 적은 함수 타입에 할당이 가능함


마치며

책의 후반부로 갈수록 실무에 어떻게 적용해야 하는지 감이 안잡히는 부분이 많았던 것 같다. 개념은 이해가 가더라도 왜 중요하고 어떤 기능을 구현할때 고려해야 하는지를 파악하는게 어려웠다. 실무 경험이 적고 아직 깊은 단계까지 타입스크립트를 이해해야 하는 기회가 없어서 필요할 때마다 자주 읽어야 한다는 생각이 들었다.

profile
개발자로 성장하기

0개의 댓글