TypeScript

들어가면서

좀 더 진지하게, 타입스크립트 언어에 대해 베이직을 제대로 정리 해놓으려고 한다. 어떤 개발자가 될지는 어쩌면 시작에서 결국 결정나니까..!

본 정리는 TypeScript 공식 문서를 요약한 내용이며, 전체 내용을 담고 있진 않으니까 레퍼 문서를 꼭 참고하실 것..!

TypeScript for Java / C# Programmers

JavaScript 함께 배우기 (Co-learning JavaScript)

  • TypeScript는 JS와 동일한 런타임을 사용하므로, 특정한 런타임 동작의 구현에 필요한 리소스는 JS에서도, TS에서도 잘 적용된다. TS에 국한하여 특정된 리소스에만 제한을 두지 말 것

클래스 다시 생각하기

  • C#, Java 는 명시적 OOP 언어이다. 그러나 TS는 이러한 방식과 차이를 가진다.

자유로운 함수와 데이터

  • JS에서부터 시작하여 함수, 데이터는 미리 정의된 클래스나, 구조에 속하지 않고 자유롭게 데이터를 전달할 수 있는 유연성을 확보하고 있다.

정적 클래스

  • C#, Java의 싱글턴과 정적 클래스 같은 특정 구조는 TS에서는 필요하지 않다.

TS 의 OOP

  • 기본적으로 TS는 클래스를 사용이 가능한 구조를 갖고 있고, OOP 계층으로 적절한 문제는 지속적으로 해결하면 된다.
  • TS 는 종례의 인터페이스, 상속, 정적 메서드 구현과 같은 일반적인 패턴을 모두 지원한다.

타입에 대해 다시 생각하기

  • 그러나 TS의 타입에 대한 이해는 C#, Java의 그것과는 다른 특성을 가지고 있다.

이름으로 구체화된 타입 시스템

  • C#, Java 는 주어진 값과 객체는 정확하게 하나의 타입을 가진다. 따라서 명시적 상속관계나 공통적으로 구현된 인터페이스가 없는 이상 두 클래스가 유사한 형태를 가졌다 해도 서로 대체하여 사용할 수 없다.
  • 이러한 양상은 reified, nominal 타입 시스템을 설명한다. 코드에서 사용한 타입은 런타임 시점에 존재하며, 타입은 구조가 아닌 선언을 통해 연관지어진다.

집합으로서의 타입

  • C#, Java에서는 런타임 타입과 해당 컴파일 타입 선언 사이의 1:1 대응 관계가 중요하다. 그러나 TS는 공통으로 무언가를 공유하는 값에 집합으로 타입을 바라보는게 낫다.

삭제된 구조적 타입

  • TS의 객체는 정확히 단일 타입이 아니다. 즉, 객체를 생성 시 둘 사이의 선언적인 관계를 배제해도 인터페이스가 예상되는 곳에 해당 객체를 사용할 수 있다.
interface Pointlike {
  x: number;
  y: number;
}
interface Named {
  name: string;
}
function printPoint(point: Pointlike) {
  console.log("x = " + point.x + ", y = " + point.y);
}
function printName(x: Named) {
  console.log("Hello, " + x.name);
}
const obj = {
  x: 0,
  y: 0,
  name: "Origin",
};
printPoint(obj); // 이게 왜 됨? ㄷㄷㄷ
printName(obj); // 이건 왜 됨? ㄷㄷㄷ
  • TS의 타입 시스템은 구조적 성격을 가지고, 명목적이지 않다.
  • TS의 타입 시스템은 구체화하지 않는다. 런타임에 objPointline임을 알려주지 않는다. 사실 Pointlike타입이란 것은 어떤 형태로도 존재하지 않는다.
  • 집합으로서 타입의 개념을 보면, 결국 각 집합의 멤버들이 모여있는 것이 타입인 것이다.

구조적 타입화의 결과

  • 구조적 타입화라는 것이 가지는 두 가지 측면이 있다.

빈 타입

class Empty {}
function fn(arg: Empty) {
  // 무엇인가를 하나요?
}
// 오류는 없지만, '빈' 타입은 아니지 않나요?
fn({ k: 10 });
  • TS는 주어진 인수가 유효한 Empty 인지 확인하여 fn 호출이 유효한지를 검사한다.
  • 그러나 여기서 놀라운 점은 모든 프로퍼티가 속해 있으니 문제 없이 유효한 호출로 인정되고, 넘어간다…!
  • 구조적 타입 시스템은 호환 가능한 유형의 속성을 갖는 측면에서 하위 타입을 설명하기 때문에 위의 관계를 암시적으로 구별해준다는 점에서 명목적 객체지향 언어와는 다른 점이다.

동일한 타입

class Car {
  drive() {
    // hit the gas
  }
}
class Golfer {
  drive() {
    // hit the ball far
  }
}
// No error?
let w: Car = new Golfer();
  • 이러한 구조도 오류 없이 동작하는데, 이유는 클래스 구조가 동일하기 때문이다. 잠재적인 혼란이 될 수 있으나, 차후 클래스 챕터에서 이에 대해 자세히 볼것이지만 일단 가능하다는 점만 알고 있을것..!

반영

  • 객체지향 프로그래머는 제네릭을 포함하여 어떤 값의 유형이라도 다룰 수 있는 코드를 써본 경험이 있을 거고 유용하게 사용할 것이다.
  • 하지만 TS의 타입 시스템이 완벽히 지워지므로, 제네릭 타입 인자의 인스턴스화와 같은 정보는 런타임에 사용할 수 없다.
  • JS의 경우 typeof, instanceof 와 같은 제한된 원시 요소가 있지만, 이런 연산자는 타입이 지워진 코드의 출력에 존재하므로 여전히 작동한다. 이때 typeof (new Car())Car"Car" 가 아닌 "object"로 표현이 된다.

The Basics

function fn(x) {
  return x.flip();
}
  • 위 코드를 보면 인자로 전달된 객체가 호출이 가능한 프로퍼티 flip을 가져야만 위 함수가 동작한다는 것을 우리는 알 수 있다.
  • 하지만 JS는 이러한 정보를 코드가 실행되는 런타임 동안엔 모른다.
  • 이런 측면에서 타입 이란 어떤 값이 fn 으로 전달 될 수 있고, 어떤 값은 실행 실패할 것임을 설명하기 위한 개념이다. Js는 오로지 동적 타입 만을 제공하지만 이에 비해 TS는 정적 타입을 제공하여 이러한 문제를 개선한다.

정적 타입 검사

  • 결국 코드 실행하기 전에 TypeError 라는 버그가 미리 발견될 도구가 있으면 좋을 것이고, TS는 이를 갖추고 있다.
const message = "hello!";
 
message();
//This expression is not callable.
//  Type 'String' has no call signatures.

예외가 아닌 실행 실패

  • 호출 가능하지 않은 것에 대하여 호출을 시도하는 경우, JS는 마치 당연하다는 듯 오류를 던저야 할 상황에서 undefined를 반환한다.
const user = {
  name: "Daniel",
  age: 26,
};
user.location; // undefined 를 반환
  • 그러나 궁극적으로 정적 타입 시스템은 이러한 문제들을 해결해준다.
const user = {
  name: "Daniel",
  age: 26,
};
 
user.location;
//Property 'location' does not exist on type '{ name: string; age: number; }'.
  • 이러한 구성이 유연성을 희생하지만 동시에 명시적 버그가 발생 여지를 최소화한다는 점에 그 목적이 내재되어 있다고 볼 수 있다.

프로그래밍 도구로서의 타입

  • 타입 검사기는 우리가 변수 또는 다른 프로퍼티 상의 올바른 프로퍼티에 접근하는지를 바로바로 검사 및 제안할 수 있다.

tsc, TypeScript 컴파일러

  • TS 설치
npm install -g typescript
  • TS 컴파일(From TS, To JS)
tsc hello.ts

오류 발생시키기

  • 코드 상에 문제가 발생하면 컴파일 시 이를 인지하고 오류 메시지를 보여준다. 이는 TypeScript의 핵심 가치에 기반한 동작이라는 점을 인지해야 한다.
  • 동시에 이러한 점을 무너뜨리고, JS처럼 하기 위한 옵션을 넣어 줄 수도 있다. 그것이 --noEmitOnError 플래그다.
tsc --noEmitOnError hello.ts

명시적 타입

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
 
greet("Maddison", Date());
//Argument of type 'string' is not assignable to parameter of type 'Date'.
  • 두번째 인자에서 오류를 보고 했는데, 이렇게 하는 이유는 Date()를 호출하면 String 을 반환하고, Date타입을 반환하지 않기 때문이다.
  • 이를 해결한 코드는 아래와 같다.
function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
 
greet("Maddison", new Date());
  • 이렇게 타입에 대한 영역에 대해 인지하고 에러를 호출하는 것은 타입 정보를 추론하는 기능이 있기 때문이기도 하다.
let msg = "hello there!";
    //let msg: string
  • 이러한 추론 기능이 new Date() 가 아닌 경우 String 임을 깨닫고 에러를 호출하며, 동시에 타입 선언이 없더라도 값을 통해 유추하는 기능을 TypeScript 가 제공해준다고 볼 수 있다.

지워진 타입

// TS 원본 greet.ts
function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
 
greet("Maddison", new Date());
// 컴파일 된 greet.js
"use strict";
function greet(person, date) {
    console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));
}
greet("Maddison", new Date());
  • TS로 만들어진 파일을 JS 로 컴파일 했을 때 위와 같은 코드가 나오게 된다. 여기서 두 가지를 알 수 있다.
    1. 각 인자는 타입 표기가 없어진다.
    2. 탬플릿 문자열 - 백틱을 사용했던 문장이 다른 문자열 형태로 바뀐다.
  • 정적 타입은 TS의 것이니 JS로 컴파일 된 상태에선 해당 하는 내용이 제거 되며, TS 전용 코드 역시 제거된다.

다운 레벨링

  • 위의 예시를 보면 백틱의 형태에서 concat으로 연결된 구조로 바뀐다.
  • 이는 TS 가 JS 최신과는 다른 하위 버전을 지향하여 되어 있기 때문이다.
  • 이때 컴파일의 타겟 플래그를 따로 설정하면, 이에 맞춰 다른 구조의 JS파일을 얻을 수 있다.
tsc --target es2015 input.ts
# es2015 버전을 기반으로 컴파일을 시켜준다. 

엄격도

  • TS는 사용자의 사용 목적에 따라 타입 검사기의 엄격도를 수정할 수 있다.
  • 엄격도를 위한 플래그는 여러가지가 존재하나, CLI 상에서 --strict 를 지정해주거나, tsconfig.jsonstrict: true 를 추가하면 보다 강하게 만들 수 있다.
  • 동시에 각각 플래그를 개별적으로 조절할 수도 있다.
    • noImplicitAny : TS 에서 타입 추론하지 않고 가장 관대한 타입을 any로 본다. 그러나 any는 문제가 많고, 따라서 해당 플래그를 활성화 하면 any를 사용하는 것, 암묵적으로 추론되는 것에 대해 오류를 발생시킨다.
    • strictNullChecks : null, undefined 와 같은 값은 다른 타입의 값에 할당 할 수 있는 것이 기본 동작이지만, 버그를 만들기 좋다. 이에 해당 플래그는 명시적으로 처리해주고, 해당 처리가 있는 없는지를 검사해준다.