[TS] 우아한 타입스크립트 세미나 - 1부
Prog. Langs & Tools/TypeScript

[TS] 우아한 타입스크립트 세미나 - 1부

지난 8월 들었었던 이웅재님의 우아한 타입스크립트 내용을 정리해서 블로그 포스팅 해 보려고 한다. 1/2부로 나누어서 세미나를 진행하였고, 포스팅도 2번에 나누어서 할 생각이다.

많은 사람들이 타입스크립트를 쓰면 버그도 사라지고, 테스트 코드를 작성하지 않아도 된다고 착각하는 경우가 있다. 이는 분명히 잘못된 생각이며, 타입스크립트를 가지고 타이핑을 잘 하면 우리가 가질 수 있는 이점은 런타임 전에 오류를 미리 파악할 수 있다는 점이다.

 

작성자와 사용자

타입스크립트의 타입 시스템은 다음과 같은 특징을 가지고 있다.

  • 타입을 명시적으로 지정할 수 있음
  • 타입을 명시적으로 지정하지 않으면, 타입스크립트 컴파일러가 자동으로 타입을 추론

우리는 함수를 가운데에 매개로 해서 구현자와 사용자를 연결한다. 때로은 이 둘 사이에서 서로 올바르지 않은 타입의 데이터를 보내고 받다 보니 오해가 생기기도 한다. 타입스크립트에서 이러한 타입 관련해서 쓸 수 있는 주요한 옵션들을 소개한다.

  • noImplicitAny : 옵션을 켜면, 타입을 명시적으로 지정하지 않은 경우, TS가 추론 중 any라고 판단하게 되면 컴파일 에러를 발생시켜 명시적으로 지정하도록 유도
// noImplicitAny: false

function foo(a) {
  return a * 38;
}

console.log(foo('Owen') + 5); // NaN

// noImplicitAny: true

function bar(b) {
  return a * 38;
}

console.log(bar('Owen') + 5); // error TS7006: Parameter 'b' implicitly has an 'any' type.
  • strictNullChecks : 옵션을 켜면, 모든 타입에 자동적으로 포함되어 있는 null과 undefined를 제거
// strictNullChecks : true

// 명시적으로 지정하지 않은 함수의 리턴 타입은 number | undefined 로 추론된다.
function foo(a: number) {
  if (a > 0) {
    return a * 38;
  }
}

// 해당 함수의 리턴 타입은 number | undefined 이기 때문에, 타입에 따르면 이어진 연산을 바로 할 수 없다.
console.log(foo(-5) + 5); // error TS2532: Object is possibly 'undefined'
  • noImplicitReturns : 옵션을 켜면, 함수 내에서 모든 코드가 값을 리턴하지 않으면, 컴파일 에러를 발생시킨다.
// noImplicitReturns : true

// if가 아닌 경우 return을 직접 하지 않고 코드가 종료

// error TS7030: Not all code paths return a value
function foo(a: number) {
  if (a > 0) {
    return a * 38;
  }
}

 

인터페이스(interface)와 타입 알리아스(type alias)

TS에서는 structural type system을 가지고 있어서, 구조가 같으면 같은 타입을 가진다. 하지만 TS에서도 구조가 같아도 이름이 다르면 다른 타입을 가지는 nominal type system을 가질 때도 있다.(규모가 큰 프로젝트에서 사용)

Nominal Type System

두 개의 타입을 동시에 사용하는 intersection이나 두 개의 타입 중 어느 하나를 사용하는 union type도 TS의 타입이 가지는 중요한 특성 중 하나이다. 주의해야 할 점은 유니온 타입은 인터페이스에서 상속받을 수가 없다는 점이다.

Intersection
Union Type

 

서브타입과 슈퍼타입

서브타입은 어떤 집합에 포함되는 집합을 의미한다. 그리고 슈퍼타입은 어떤 집합보다 더 큰 범위의 집합을 의미한다.

never라는 타입을 주목해서 볼 필요가 있다. never 타입에는 어떠한 것도 할수 없다. 그리고 never는 모든 타입이 서브타입으로 가지고 있는 타입이다.

// sub 타입은 sup 타입의 서브 타입니다.
// sup 타입은 sub 타입의 슈퍼 타입이다.
let sub: never = 0 as never;
let sup: number = sub;
sub = sup; // error! Type 'number' is not assignable to type 'never'

원시타입인 경우는 비교가 쉽지만 object의 경우는 조금 유심히 보아야 한다. object는 각각의 프로퍼티가 대응하는 프로퍼티와 같거나 서브타입인 경우 할당이 가능하다.

let sub2 = { a: string; b: number; } = { a: '', b: 1 };
let sup2 = { a: string | number; b: number } = sub2;

또한 함수의 매개변수 타입만 같거나 슈퍼 타입인 경우, 할당이 가능하다. 이를 반병이라고 한다. 여기에 strictFunctionType 옵션을 켜면, 세 번째 tellme 함수는 에러를 뱉는다. 이 옵션은 함수의 매개변수 타입만 같거나 슈퍼타입인 경우가 아닌 경우, 에러를 통해 경고한다.

any 타입은 입력을 자유롭게 할 수 있는 타입이다. 함수 구현이 자유롭지만 때로는 모두 다 허용해서 문제가 발생하기도 한다. 그래서 이럴 경우 unknown 타입을 대신 사용할 수 있는데, 입력은 마음대로 하고 항상 에러를 발생시킴으로서 함수 구현에서 문제가 없도록 한다.

any 대신 unknown

타입 추론

let과 const로 변수를 선언하면 let은 원시 타입으로, const는 literal type으로 타입 추론이 다르게 지정 된다.

let a = 'Mark'; // string
const b = 'Mark'; // 'Mark' => literal type

let c = 10; // number
const d = 10; // 10 => literal type

let e = ['AA', 'BB']; // string[]
const f = ['AA', 'BB']; // string[]

const g = ['AA', 'BB'] as const; // readonly ['AA', 'BB']

TS는 Best common type이라는 공통적인 타입을 추론해 내는 특징이 있다. 이는 클래스에도 적용해 볼 수 있다.

let j = [0, 1, null]; // (number | null)[]
const k = [0, 1, null]; // (number | null)[]

Best common type

 

타입 가드(type guard)

타입 가드 방식은 여러가지가 있다. 보통 원시타입의 경우 typeof를 사용한다.

클래스의 인스턴스를 추론할 때는 instanceof를 사용한다. 특히 예외 처리를 할 때 많이 쓰인다.

오브젝트에 프로퍼티가 있는지 유무로 처리하는 in operator 타입 가드도 있다. 그리고 오브젝트의 프로퍼티가 같고 타입이 다른 경우는 literal 타입 가드를 사용하기도 한다.

in operator type guard
literal type guard

이러한 방법으로 타입 가드를 만들기 어렵다면, 직접 custom 하게 만들면 된다. 이러한 라이브러리들이 굉장히 많은데 대표적으로 lodash가 있다.

 

참고자료

우아한 타입스크립트 세미나 (이웅재님)