이펙티브 타입스크립트- 1장 타입 스크립트 알아보기

fullth·2022년 7월 20일
0

Effective TypeScript

목록 보기
1/6

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

  • 타입스크립트는 문법적으로도 자바스크립트의 상위 집합이다.
    • 문법 오류가 없는 자바스크립트 프로그램이면 유효한 타입스크립트 프로그램이다.
      .js의 코드는 이미 타입스크립트라고 할 수 있다.
    • main.js를 main.ts라고 변경해도 달라지는 것은 없다. (이 특성은 migration(8장)에 엄청난 이점을 가져다준다.)
    • 하지만, 일부 자바스크립트만이 타입 체커를 통과한다.
  • 문법의 이슈와 동작의 이슈는 독립적인 문제이다.
    • 이슈가 있는 자바스크립트 프로그램은 타입 체커에게 지적당할 가능성이 높다.
    • 아래의 코드처럼 오류의 원인을 추측할 수는 있지만, 항상 정확하지는 않다. 
    • 그래서, 명시적으로 blogs를 선언하여 의도를 분명하게 하는 것이 좋다.
const blogs = [
    {nema: 'tistory'},
    {nema: 'velog'},
    {nema: 'blog'}
];

for(const blog of blogs) {
    console.log(blog.name);
              	  //~~~~~~'name'속성이 ...형식에 없습니다.
                  //'nema'를 사용하시겠습니까?
};

/** 개선: 명시적으로 blog를 선언 */
interface Blog {
    name: string
};

const blogs: Blog[] = [
    {nema: 'tistory'}
   //~~~~~~~~~~~~~~~~ 객체 리터럴은 알려진 속성만 지정할 수 있지만,
   //                 'Blog' 형식에 'nema'(이)가 없습니다.
   //                 'name'을(를) 쓰려고 했습니까?
];
  • 타입스크립트는 자바스크립트와는 별도의 문법을 갖으므로,
    명제 '모든 자바스크립트 프로그램은 타입스크랩트 프로그램이다.'라는 참인 명제의 역은 거짓이다.
  • 타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것이다.

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

  • 타입스크립트 컴파일러는 매우 많은 설정을 갖고 있다.
    • 이 설정에 따라 타입 체커를 통과하는 기준이 바뀐다.
    • 커맨드 라인에서 사용할 수 있고, tsconfig.json 설정 파일을 통해 가능하다.
  • 가급적 설정 파일을 사용하는 것이 좋다.
    • tsc --init 명령어를 이용해 간단하게 생성할 수 있다.
  • 대부분의 설정은 어디서 소스 파일을 찾을지, 어떤 종류의 출력을 생성할지 제어한다.
  • 언어 자체의 핵심 요소들을 제어하기도 한다.
    • 대부분의 언어에서는 허용하지 않는 고수준 설계의 설정이다.
  • 되도록, noImplictAny를 설정해야 한다.
    • 타입스크립트는 타입 정보를 가질 때 가장 효과적이다.
  • 공동 프로젝트를 진행하는 도중 공유한 예제의 동작 혹은 오류가 재현되지 않으면, 컴파일러 설정이 동일한 지부터 확인해야 한다.
// ex.)
{
 "compilerOptions": {
    "noImplictAny": true,
    "strictNullChecks": true
  }
}

ITEM03 코드 설정과 타입이 관계없음을 이해하기

  • 타입스크립트 컴파일러는 크게 두 가지 역할을 수행함. 이 두 가지는 서로 독립적.
    • 최신 타입스크립트/자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일.
    • 코드의 타입 오류를 체크
  • 타입스크립트가 자바스크립트로 변환될 때 코드 내의 타입에는 영향을 주지 않음.
    • 실행 시점에도 타입은 영향을 미치지 않음.
    • 타입 오류가 있는 코드도 컴파일이 가능함.
  • 런타임에는 타입 체크가 불가능함.
    • 자바스크립트로 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 그냥 제거됨.
    • 타입을 명확하게 하려면 런타임에 타입 정보를 유지하는 방법이 필요함.
interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
                    // ~~~~~~~~~ 'Rectangle' only refers to a type,
                    //           but is being used as a value here
    return shape.width * shape.height;
                    //         ~~~~~~ Property 'height' does not exist
                    //                on type 'Shape'
  } else {
    return shape.width * shape.width;
  }
}
  • Rectangle은 타입이기 때문에 런타임 시점에는 아무런 역할을 할 수 없음.
  • heigth 속성이 존재하는지 체크해봄으로써 런타임에 타입을 유지할 수 있음.
interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
  if ('height' in shape) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}
  • 태그 기법
    • 런타임에 접근 가능한 정보를 명시적으로 저장하는 기법이다.
    • 아래의 예시와 같이 접근할 정보를 저장해두는 것.
interface Square {
  kind: 'square';
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  height: number;
  width: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape.kind === 'rectangle') {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}
  • 타입을 클래스로 만들면, 타입과 값을 둘 다 사용할 수 있다.
    • 위의 예시에서 사용한 interface들을 class로 만들어주면 된다.
    • 인터페이스는 타입으로만 사용이 가능하지만, Rectangle을 클래스로 선언하면 타입과 값으로 모두 사용할 수 있다.
class Square {
  constructor(public width: number) {}
}
class Rectangle extends Square {
  constructor(public width: number, public height: number) {
    super(width);
  }
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;  // OK
  }
}
  • 타입 연산은 런타임에 영향을 주지 않는다.
  • 런타임 타입은 선언된 타입과 다를 수 있다.
  • 타입스크립트 타입으로는 함수를 오버로드 할 수 없음.
    • 타입과 런타임의 동작이 무관함. 즉, 함수 오버로딩 불가.
    • 하나의 함수에 대해 여러 선언문을 작성할 수 있지만, 구현체는 오직 하나.
  • 타입과 타입 연산자는 변환 시 제거됨으로 성능에 영향을 주지 않지만, 타입스크립트 컴파일러는 빌드타임 오버헤드가 있음.

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

  • 자바스크립트는 기본적으로 덕 타이핑
    • "만약 어떤 새가 오리처럼 걷고, 꽥꽥거리면 난 그 새를 오리라고 부를 것."
    • 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우, 객체를 해당 타입에 속하는 것으로 간주.
    • 만약 어떤 함수의 매개변수 값이 모두 제대로 주어지면, 그 값이 어떻게 만들었는지 신경 쓰지 않고 사용.
  • 타입스크립트는 매개변수 값이 요구사항을 만족하면, 타입이 무엇인지 신경 쓰지 않는 동작을 그대로 모델링.
  • 다음 예시에서 NameVector는 x, y 타입을 갖기 때문에, calculateLength( )를 호출할 수 있음.
    • Vector2D와 NamedVector의 관계를 전혀 선언하지 않고, NamedVector를 위한 별도의 메서드를 구현하지 않아도 됨.
    • NamedVector와 Vector2D의 구조가 호환되기 때문임. 구조적 타이핑이라는 말은 여기서 나옴
interface Vector2D {
  x: number;
  y: number;
}
function calculateLength(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: 'Zee' };
calculateLength(v);  // OK, result is 5
  • 구조적 타이핑 때문에 문제가 발생하기도 함.
    • 함수를 작성할 때, 호출에 사용되는 매개변수들의 속성들이 매개변수의 타입에 선언된 속성만을 가질 것이라 생각하면 안 됨.
    • 타입은 열려있음. 확장에 열려있다는 의미. 
    • 고양이 타입에 크기 속성을 추가해서 뚱냥이가 되어도 고양이. (아이템 7 참조)
  • 구조적 타이핑은 테스트에 유리함.
  • 예시.) DB에 쿼리하고 결과 처리하는 함수
    • getAuthors 함수를 테스트하기 위해 모킹 한 PostgreDB를 생성하는 것이 아닌,
      구조적 타이핑을 이용해 구체적인 인터페이스를 정의하는 것이 더 나음.
    • 해당 방법으로 라이브러리 간의 의존성을 분리할 수 있음. (아이템 51 참조)
interface PostgresDB {
  runQuery: (sql: string) => any[];
}
interface Author {
  first: string;
  last: string;
}
interface DB {
  runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

test('getAuthors', () => {
  const authors = getAuthors({
    runQuery(sql: string) {
      return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
    }
  });
  expect(authors).toEqual([
    {first: 'Toni', last: 'Morrison'},
    {first: 'Maya', last: 'Angelou'}
  ]);
});

ITEM05 any 타입 지양하기

  • any 타입에는 타입 안전성이 없음.
    • as any를 사용함으로써 number타입에 string 데이터를 할당할 수 있음 -> 혼돈
   let age: number;
   age = '12';
// ~~~ Type '"12"' is not assignable to type 'number'
   age = '12' as any;  // OK
  • any는 함수 시그니처를 무시함.
    • 아래 예제를 보면 calculateAge( )는 Date값을 받아야 하지만, any타입이 무시함.
function calculateAge(birthDate: Date): number {
  // COMPRESS
  return 0;
  // END
}

let birthDate: any = '1990-01-19';
calculateAge(birthDate);  // OK
  • any를 사용하면 언어 서비스가 적용되지 않음. 
  • any는 코드 리팩터링 때 버그를 감춤.
  • any는 타입 설계를 감춤.
    • 설계가 잘 되었는지, 어떻게 되어 있는지 파악하기 힘듦.
  • any는 타입 시스템의 신뢰도를 떨어뜨림.
    • any를 사용하는 것으로 인해서 타입 체크를 통과하고, 런타임 중에 오류를 발생시키면 타입 체커를 신뢰할 수 없게 될 것.
  • any는 최대한 사용을 피할 것!
profile
Web Backend Developer

0개의 댓글