[TS] The Basics

chaevivi·2023년 2월 23일
0
post-thumbnail
  • JavaScript의 각각의 모든 값은 다양한 연산들이 진행되면서 관찰할 수 있는 행동 집합들을 가지고 있다. 이 말이 모호할 것이다, 하지만 예를 들어보면, 변수 message에서 실행할 수 있는 연산자가 있다고 고려해보자.
    // 'message'에서 'toLowerCase' 프로퍼티에 접근한 다음 호출
    message.toLowerCase();
    
    // 'message' 호출
    message();
    위의 코드를 분해하면, 코드의 첫번째 라인은 toLowerCase라는 프로퍼티에 접근하고 호출한다. 두번째 라인은 message를 직접적으로 호출한다. 하지만 message라는 값을 모른다고 가정하면, 이 코드를 실행하려고 할 때 어떤 결과를 얻을 수 있을지 확신할 수 없다. 각 연산의 동작은 처음에 어떤 값을 가졌는지에 전적으로 의존한다.
    • message를 호출할 수 있을까?

    • messagetoLowerCase라는 프로퍼티를 가지고 있는가?

    • 만약 그렇다면, toLowerCase를 호출할 수 있을까?

    • 두 값들 모두 호출할 수 있다면, 어떤 것을 리턴할까?

      이 질문들의 답은 일반적으로 JavaScript를 작성할 때 머리에 떠오를 것이고, 모든 디테일들이 올바르길 바라야 한다.

      message가 아래처럼 정의되었다고 해보자.

      const message = "Hello World!";

      message.toLowerCase()를 호출하면, 소문자로 된 똑같은 문자열을 얻을 수 있을 것이다.

      맨 위 코드의 두 번째 라인 message() 는 어떨까? 만약 JavaScript에 익숙하다면, 이 코드가 실패한다는 것을 알 것이다.

      TypeError: message is not a function

      이런 실수를 피할 수 있으면 얼마나 좋을까?

      코드를 실행할 때, JavaScript 런타임이 무엇을 할 지 결정하는 방법은 값의 type (어떤 종류의 동작 및 기능이 있는지)을 파악하는 것이다. 이것이 TypeError가 암시하는 것 (문자열 “Hello World!”는 함수로 호출할 수 없다)의 일부이다.

      원시값인 stringnumber 같은 몇몇 값들은 런타임에 typeof 연산자를 사용해서 그들의 type을 알 수 있다. 하지만 함수같은 것들은, 그들의 type을 알 수 있는 런타임 메커니즘이 없다. 예를 들어, 아래의 함수를 보자.

      function fn(x) {
      	return x.flip();
      }

      우리는 위 코드를 읽으면서, 호출 가능한 flip 프로퍼티가 있는 객체가 주어지는 경우에만 함수가 동작한다는 것을 알 수 있지만, JavaScript는 코드가 실행되는 동안 확인할 수 있는 방법으로 이 정보를 표시하지 않는다. 순수 JavaScript에서 fn 함수가 특정 값으로 수행하는 작업을 알려주는 유일한 방법은 해당 값을 호출하고 어떤 일이 발생하는지 확인하는 것 뿐이다. 이러한 동작은 코드가 실행되기 전에 어떤 일을 하는지 예측하기 힘들고, 이것은 코드를 작성하는 동안에 코드가 어떤 일을 하는지 알기 힘들 다는 것을 의미한다.

      type은 어떤 값이 fn 함수에 전달될 수 있고, 어떤 값이 충돌하는지 설명해주는 개념이다. JavaScript는 동적 타이핑(어떤 일이 일어나는지 코드를 실행해야 한다)만을 제공한다.

      대안책은 정적 타입 시스템을 사용하여 실행되기 전에 예상되는 코드를 예측하는 것이다.


정적 타입 체크 (Static type-checking)

  • string을 함수처럼 호출하려 해서 발생한 TypeError를 다시 생각해보자. 대부분의 사람들은 코드가 동적하면서 발생하는 에러는 어떤 종류든 싫어한다. 에러는 버그로 간주된다. 그리고 새 코드를 작성할 때, 새 버그를 피하기 위해 최선을 다한다. 짧은 코드를 추가하고, 파일을 저장하고, 코드를 재실행하고, 즉시 에러를 확인한다면, 문제를 재빠르게 격리할 수 있지만 항상 그런 것은 아니다. 기능을 충분히 철저하게 테스트하지 않아서 발생할 수 있는 잠재적인 에러가 실제로 발생하지 않을 수도 있다! 혹은 운이 좋아서 에러를 목격한다면, 결국 대규모의 리팩토링을 할 것이고 파헤쳐야 하는 많은 코드를 추가할 것이다. 이상적으로, 코드가 동작하기 전에 이러한 버그들을 찾을 수 있는 도구가 있다. TypeScript같은 정적 타입 체크가 하는 일이다. 동적 타입 시스템은 프로그램을 실행할 때의 값들의 모양과 행동을 알려준다. TypeScript같은 타입 체크는 해당 정보를 이용하고 일이 제대로 진행되지 않을 때 알려준다.
    const message = "hello!";
    
    message();
    This expression is not callable,
    Type 'String' has no call signatures.
    마지막 샘플 코드를 TypeScript에서 실행하면, 코드를 실행하기 전에 에러 메시지를 띄워 준다.

Non-exception Failures

  • 지금까지 우리는 JavaScript 런타임이 무의미하다고 생각한다고 알려주는 경우인 런타임 에러와 같은 특정 사항에 대해 논의했다. 이 경우는 ECMAScript 사양에 예상하지 못한 상황이 발생했을 때 언어가 어떻게 동작해야 하는지에 대한 명시적인 지시사항이 있기 때문에 발생한다. 예를 들어, 사양은 호출할 수 없는 것을 호출하려 할 때 에러를 발생시키라고 말한다. “명백한 동작”처럼 들릴 수 있지만, 객체에 존재하지 않는 프로퍼티에 접근하는 것 또한 에러를 발생시킬 것이라고 상상할 수 있다. 하지만 JavaScript는 다른 행동을 취할 것이고, undefined 값을 반환한다.
    const user = {
    	name: "Daniel",
    	age: 26,
    };
    
    user.location;  // returns undefined
    결국, 정적 타입 시스템은 비록 “유효한” JavaScript가 즉시 에러를 발생시키지 않아도, 해당 시스템에서 오류로 표시되어야 하는 코드를 호출해야 한다. TypeScript 에서는, 다음 코드에서 loaction이 정의되지 않은 것에 대한 에러를 생성한다.
    const user = {
    	name: "Daniel",
    	age: 26,
    };
    
    user.location; 
    Property 'location' does not exist on type '{ name: string, age: number; }'.
    가끔 그것은 당신이 표현할 수 있는 거래를 의미하지만, 의도는 프로그램 안에서 합법적인 버그를 잡는 것이다. 그리고 TypeScript는 합법적인 버그들을 많이 잡는다. 예를 들어, 오타,
    const announcement = "Hello World!";
    
    // 오타를 얼마나 빨리 발견할 수 있는가?
    announcement.toLocaleLowercase();
    announcement.toLocalLowerCase();
    
    // 이렇게 쓰려고 했을 것이다...
    announcement.toLocaleLowerCase();
    호출되지 않은 함수들,
    function flipCoin() {
    	// Math.random()
    	return Math.random < 0.5;
    	Operator '<' cannot be applied to types '() => number' and 'number'.
    }
    기초 로직 에러들
    const value = Math.random() < 0.5 ? "a" : "b";
    if (value !== "a") {
    	// ...
    } else if (value === "b") {
    	This condition will always return 'false' since types '"a"' and '"b"' have no overlap.
    }

Types for Tooling

  • TypeScript는 코드에서 실수를 했을 때 버그를 잡을 수 있다. 그것은 좋다, 하지만 TypeScript는 처음부터 그러한 실수들을 하지 않도록 예방해줄 수 있다. 타입 체크는 변수나 다른 프로퍼티들에 정확한 프로퍼티에 접근했는지와 같은 사항을 체크한 정보를 가지고 있다. 그 정보를 가지고 있으면, 타입 체크는 어떤 프로퍼티를 사용할 것인지 제안을 해줄 수 있다. 이것은 코드 편집에서도 TypeScript를 활용할 수 있고, 핵심 타입 체크가 편집기에서 작성할 때 에러 메시지와 코드 완성을 제공해준다는 것을 의미한다. 이것이 사람들이 TypeScript의 도구에 대해 이야기할 때 자주 언급하는 부분이다. TypeScript는 툴링을 중요하게 생각하고, 그것은 작성할 때 완성과 오류를 넘어서게 한다. TypeScript를 지원하는 편집기는 자동으로 에러를 고치기 위해 “빠른 수정”, 코드를 쉽게 재구성할 수 있는 리팩토링, 변수 정의로 이동하거나 주어진 변수의 모든 참조를 찾는데 유용한 탐색 기능을 제공할 수 있다. 이 모든 것은 타입 체크의 맨 위에서 구축되고, 완전한 교차-플랫폼이라서, 당신이 가장 좋아하는 편집기에서 TypeScript를 사용할 수 있을 것이다.

tsc, the TypeScript compiler

  • 우리는 지금까지 타입 체크에 대해 이야기했다, 하지만 아직 타입 체크를 사용하지는 않았다. 이제부터 TypeScript 컴파일러인 tsc와 친해져보자. 첫번째로, npm으로 설치한다.
    npm install -g typescript
    • 이것은 TypeScript 컴파일러 tsc를 전역적으로 설치한다. 대신에 지역 node_modules 패키지로 tsc를 구동하기를 원한다면 npx나 비슷한 도구를 사용할 수 있다.

      이제 빈 폴더에 옮기고 첫번째 TypeScript 프로그램 hello.ts를 작성해보자.

      console.log("Hello world!");

      “hello world” 프로그램은 JavaScript에서 작성한 “hello world” 프로그램과 동일한 것처럼 보일 것이다. 이제 typescript 패키지에 의해 설치된 tsc 커맨드를 작동해 타입 체크를 해보자.

      tsc hello.ts

      우리는 tsc를 실행했고 어떠한 일도 일어나지 않았다! 타입 에러도 없어서 보고할 것이 없기 때문에 console에는 어떠한 출력도 없을 것이다.

      하지만 다시 체크해보면, 대신에 우리는 출력 파일 하나를 얻는다. 현재 디렉토리에서 보면, hello.js 파일 옆에 hello.ts 파일을 볼 수 있을 것이다. 이러한 출력 파일은 tsc가 순수 JavaScript 파일로 컴파일하거나 변환 후 hello.ts 파일의 출력이다. 그리고 내용을 체크해보면, TypeScript가 .ts파일을 처리한 후 출력되는 내용을 볼 수 있다.

      이러한 경우, TypeScipt는 변경할 것이 조금밖에 되지 않아서 우리가 작성한 것과 동일해 보이는 것이다. 컴파일러는 사람이 작성한 것처럼 읽을 수 있는 코드로 깨끗하게 만들려고 한다. 항상 쉬운 것은 아니지만, TypeScript는 일관되게 들여쓰기를 하고, 코드가 여러 줄에 걸쳐 있는 경우, 주변에 주석을 달려고 한다.

      만약 타입 체크 에러를 발생시키면 어떨까? hello.ts를 다시 써보자.

      function greet(person, date) {
      	console.log(`Hello ${person}, today is ${date}`);
      }
      
      greet("Brendan");

      tsc hello.ts 를 실행해보면, 커맨드라인에 에러가 뜨는 것을 볼 수 있다.

      Expected 2 arguments, but got 1

      TypeScript는 greet 함수에 인자를 넘겨주는 것을 잊었다고 말해주고 있고 당연히 그렇다. 그래서 표준 JavaScript만으로 작성하면, 타입 체크는 우리의 코드에서 문제점를 찾을 수 있다.


Emmiting with Errors

  • 마지막 예제에서 알아채지 못했을 한가지는 hello.js 파일이 다시 바뀌었다는 것이다. 파일을 열면 내용은 여전히 입력 파일과 같아 보일 것이다. 놀랄만한 것은 tsc는 코드에 대한 에러를 보고했다는 사실이다, 하지만 이것은 TypeScript의 핵심 가치 중 하나에 기반해 있다. 이전부터 반복하려면, 타입을 체크하는 코드는 실행할 수 있는 프로그램의 종류를 한정시켜서, 타입 체크가 어떤 종류의 것들을 받아 들일 수 있는 지에 대한 거래가 있다. 대부분은 괜찮겠지만, 체크가 방해가 되는 시나리오가 있다. 예를 들어, JavaScript 코드를 TypeScript 코드로 바꾸고, 타입 체크 에러를 발생시켰다고 가정해보자. 마침내 타입 체크를 위해 일을 정리하겠지만 원래의 JavaScript 코드는 이미 작동중이다! TypeScript로 변환하면 실행이 중지되는 이유는 무엇일까? 그래서 TypeScript는 방해가 되지 않는다. 물론, 시간이 지나면 실수에 대해 좀 더 방어적일 수 있고, TypeScript를 좀 더 엄격하게 행동하도록 만들 수도 있다. 이러한 경우에는 noEmitOnError 컴파일러 옵션을 사용할 수 있다. hello.ts 파일을 바꿔보고 tsc를 실행해보아라.
    tsc --noEmitOnError hello.ts
    hello.js가 업데이트 되지 않는 다는 것을 알 수 있을 것이다.

Explicit Types

  • 지금까지, TypeScript에게 persondate가 어떤 타입인지 말하지 않았다. TypeScript에게 personstring이고 dateDate 객체라는 것을 말해주자. datetoDateString() 메서드도 사용할 것이다.
    function greet (person: string, date: Date) {
    	console.log(`Hello ${person}, today is ${date.toDateString}!`);
    }
    greet이 호출할 수 있는 변수의 타입을 알려주기 위해 persondate의 타입을 명시해주었다. “greetstring 타입의 personDate 타입의 date를 취할 수 있다.” TypeScript는 greet이 잘못 호출할 수 있는 다른 경우에 대해 알려줄 수 있다. 예를 들어,
    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'.
    TypeScript는 두번째 인자에 대한 에러를 알려주었다, 왜 그런 것일까? 아마, JavaScript에서 Date()를 호출하는 것은 string값을 리턴하기 때문이다. 반면에, new Date()로 Date를 생성하면 실제로 우리가 예상하고 있는 값을 준다. 에러를 고치면,
    function greet (person: string, date: Date) {
    	console.log(`Hello ${person}, today is ${date.toDateString}!`);
    }
    
    greet("Maddison", new Date());
    우리는 항상 명시적 타입 표시를 작성할 수 없다는 것을 명심해라. 많은 경우에, TypeScript는 우리가 타입을 생략하더라도, 타입을 유추(또는 파악)할 수 있다.
    let msg = "hello there!";
    // let msg: string
    TypeScript에게 msg 타입이 string이라는 것을 알려주지 않아도, 그것을 알아낼 수 있다. 타입 시스템이 동일한 타입을 유추할 수 있는 경우는 타입 명시를 추가하지 않는 것이 가장 좋다.

Erased Types

  • tsc를 JavaScript를 출력하여 함수 greet을 컴파일했을 때 어떤 일이 발생하는지 알아보자.
    "use strict";
    function greet (person, date) {
    	console.log("Hello ".concat(person, ", today is ").concat(date.toDateString()));
    }
    greet("Maddison", new Date());
    우리는 두가지를 알 수 있다.
    1. person과 date 파라미터는 더이상 타입 명시를 하지 않았다.

    2. 템플릿 스트링( )은 연결이 있는 일반 문자열로 바뀌었다.

      첫 번째 사실에 주목해보자. 타입 명시는 JavaScript (또는 ECMAScript)의 부분이 아니다, 그래서 어떠한 브라우저나 런타임도 수정하지 않은 TypeScript를 실행할 수 없다. 이 이유 때문에 TypeScript는 컴파일러가 필요하다. TypeScript를 없애거나 실행할 수 있는 특수한 코드로 바꾸는 방법이 필요하다. 대부분의 TypeScript-특수 코드는 지워지고, 마찬가지로 타입 명시가 완전히 지워진다.

    • 타입 명시는 프로그램에서 런타임 행위를 변경시키지 않는다는 것을 기억해라.

Downleveling

  • 위와 또 다른 점은 템플릿 스트링을 이 코드에서
    `Hello ${person}, today is ${date.toDateString()}!`;
    이 코드로 재작성되었다는 것이다.
    "Hello " + person + ", today is " + date.toDateString() + "!";
    왜 이런 일이 일어날까? 템플릿 스트링은 ECMAScript2015(ECMAScript 6, ES2015, ES6 등) 버전의 기능이다. TypeScript는 새 버전의 ECMAScript를 ECMAScript3이나 ECMAScript5(ES3, ES5)과 같은 예전 버전으로 코드를 재작성할 수 있는 능력이 있다. 새롭거나 “높은” 버전의 ECMAScript를 오래되거나 “낮은” 버전으로 바뀌는 과정은 downlevling이라고 불린다. 기본 TypeScript는 ES3을 타겟으로 나왔는데, ES3은 ECMAScript에서 매우 오래된 버전이다. 우리는 traget 옵션을 사용해서 조금 더 최신의 것을 선택할 수 있다. —target es2015를 실행시키면 ECMAScript2015를 지원하는 어느 곳에서든 의미한 코드를 실행시킬 수 있도록, ECMAScript2015를 타겟으로 한 TypeScript로 바꿀 수 있다. tsc—target es2015 hello.ts를 실행시키면 아래의 출력을 얻을 수 있다.
    function greet(person, date) {
      console.log(`Hello ${person}, today is ${date.toDateString()}!`);
    }
    greet("Maddison", new Date());


출처
(en) https://www.typescriptlang.org/docs/handbook/2/basic-types.html#explicit-types

profile
직접 만드는 게 좋은 프론트엔드 개발자

0개의 댓글