TypeScript

h232ch·2022년 10월 9일
0

frontend

목록 보기
3/3
post-thumbnail

The Basic


자바스크립트의 모든 값에는 서로다른 작업을 실행하여 관찰할 수 있는 일련의 동작이 존재한다. 추상적으로 들리겠지만 아래의 예시 코드를 보면 message 변수에서 실행 가능한 몇가지 동작을 예측할 수 있다.

// Accessing the property 'toLowerCase'
// on 'message' and then calling it
message.toLowerCase();
// Calling 'message'
message();

자바스크립트에서는 일반적으로 message 변수가 무엇을 의미하는지 모르기 때문에 코드가 실행되었을때 확실하게 어떤 결과가 나올지 예측할 수 없다.

  • message 변수는 호출 가능한가?
  • message 변수는 toLowerCase 프로퍼티를 가지고 있는가?
  • 만약 그렇다면 toLowerCae는 호출 가능한가?
  • 둘다 그렇다면 해당 변수들은 무엇을 리턴하는가?

이러한 질문은 자바스크립트 코드를 생성할 때 일반적으로 갖게되며 우리는 모든 세부사항을 제대로 알고 싶어한다. (에러를 사전에 방지하기 위해..)

message 변수가 아래와 같이 정의 되었을 경우

const message = "Hello World!";

예시의 message.toLowerCase()는 잘 동작하여 소문자로 변환된 문자열을 보여준다. 반면 messag()는 아래와 같은 TypeError를 보여준다.

TypeError: message is not a function

위와 같은 에러를 방지할 수 있으면 매우 좋을 것이다.

지바스크립트 런타임이 수행할 작업을 선택하는 방식은 값의 유형, 즉 어떤 종류의 동작과 기능이 있는지 파악하는 것이다. 이것이 TypeError가 암시하는 것의 일부이고 "Hello World!"라는 문자열은 함수로 호출할 수 없다는 것을 의미한다.

string, number와 같은 원시 타입의 변수들은 런타임에 typeof를 이용해서 유형을 식별할 수 있지만 function의 경우 런타임에 유형을 식별할 수 있는 적당한 메커니즘이 존재하지 않는다.

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

예시 코드를 보면 fn 함수는 반드시 flip 프러퍼티가 존재해야 동작하지만 자바스크립트는 우리가 코드를 싱행하는 동안 해당 정보를 표시하지 않는다. 순수 자바스크립트에서 해당 정보를 확인할 수 있는 방법은 해당 코드를 실행하고 결과를 확인하는 것 외에는 방법이 없다. 이러한 종류의 동작은 코드를 실행하기 전에 코드가 어떻게 동작할지 예측하기 어렵게한다. 즉, 코드 작성중 코드가 수행할 작업을 알기가 더 어려워진다는 것을 의미한다.

이렇게 보면, 타입은 어떤 변수가 fn을 통과할지, 통과하지 못할지를 설명하는 것이다. 자바스크립트는 오직 dynimic typing(코드를 실행하여 어떤일이 벌어지는 지 확인하는 방법)만 지원한다.

이것의 대안으로 Static type system을 사용해서 코드가 실행되기 전에 코드의 예상된 동작을 예측하는 것이 필요하다.

Static type-checking

string을 변수로 호출하다가 TypeError를 마주하게된 것을 생각해보면 많은 사람들이 코드를 실행할 때 이러한 에러를 확인하는 것을 좋아하지 않을 것이다. 이것은 버그를 의미하고 코드를 작성할 때 새로운 버그를 없애고자 더 노력할 것이다.

만약 소규모 코드를 추가, 저장, 재시작하고 즉시 에러를 확인할 수 있다면 아마도 에러를 신속하게 해결할 수도 있을것이지만 항상 그런것만은 아니다. 기능을 충분히 테스트해보지 못하여 아직 실행되지 않은 잠재적 오류가 존재하는 경우도 있고 운이좋게 오류를 발견했다 하더라도 거대한 리펙토링과 코드를 파헤치기 위한 추가 코드를 작성해야 한다.

이상적으로 코드를 실행하기 전에 에러를 찾을수 있게 도와주는 툴이 필요한데 이것이 바로 타입스크립트와 같은 static type-checker가 하는 일이다.

Static type system은 우리가 코드를 실행할 때 변수가 어떤 유형이고, 동작을 하는지 설명한다. 타입스크립트와 같은 type-checker는 해당 정보를 사용하여 문제 발생이 예상될 때 알려준다.

const message = "hello!";
 
message();

This expression is not callable.
  Type 'String' has no call signatures.

타입스크립트로 실행되는 위 예제는 코드가 실행되기 전에 에러 메세지를 전달한다.

Non-exception Failures

지금까지 명확한 런타임 에러에 대해서 이야기 했다면 이번 케이스는 ECMAScript 명세가 가지고 있는 구조 때문에 발생하는 문제이다.

예를들어, 명세에는 호출이 불가능한 것을 호출했을때 에러를 던지게 되어있다. 여기서 값이 없는 프러퍼를 접근할 경우에도 동일하게 에러를 던질 것으로 예상하지만 그렇지 않다. 대신 자바스크립트는 아래와 같이 undefined를 제공한다. 해당 코드는 런타임에 예상하지 못한 에러를 발생시킬 수 있는 잠재적 에러 코드로 볼 수 있다.

const user = {
  name: "Daniel",
  age: 26,
};
user.location; // returns undefined

타입스크립트에서 다음 코드는 정의되지 않은 location에 대한 오류를 발생시킨다.

const user = {
  name: "Daniel",
  age: 26,
};
 
user.location;

타입스크립트는 다양한 방법으로 많은 에러를 찾아낸다.
For Example : Typos,

const announcement = "Hello World!";
 
// How quickly can you spot the typos?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();
 
// We probably meant to write this...
announcement.toLocaleLowerCase();

uncalled functions,

function flipCoin() {
  // Meant to be Math.random()
  return Math.random < 0.5;
Operator '<' cannot be applied to types '() => number' and 'number'.
}

or basic logic error,

const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
  // ...
} else if (value === "b") {
This condition will always return 'false' since the types '"a"' and '"b"' have no overlap.
  // Oops, unreachable
}

Type for Tooling

타입스크립트는 코드에서 우리가 작성한 실수를 찾아준다. 또한 첫 시작부터 실수를 예방할 수 있게 도와준다.

type-checker는우리가 올바른 변수 및 프로퍼티에 접근하고 있는지에 대한 정보가 있다. 한번 이 정보를 갖게되면 우리가 사용하고자 하는 프로퍼티를 추천해준다.

타입스크립트는 도구를 중요하게 생각하며, 이는 입력시 완성을 높이고 에러를 예방한다. 타입스크립트를 지원하는 에디터는 자동으로 에러를 수정하고 코드를 리펙터링하기 위한 "quick fixes" 기능을 제공하며 변수의 정의로 이동하거나 지정된 변수의 레퍼런스를 찾기 위한 유용한 탐색 기능을 제공한다. 이 모든 기능은 type-checker를 기반으로 제작되며 완전한 크로스 플랫폼으로 즐겨 사용하는 편집기에서 타입스크립트를 사용할 수 있다.

tsc, The TypeScript Compiler

타입스크립트 컴파일러인 tsc에 대해서 알아보자. 먼저 npm을 설치하고 아래와 같이 타입스크립트를 설치한다.

npm install -g typescript

빈 폴더로 이동해서 첫 타입스크립트를 작성한다. hello.ts

// Greets the world.
console.log("Hello world!");

"hello world" 프로그램은 자바스크립트에서 작성한 "hello world" 프로그램과 동일한 것 같이 보인다. 지금 tsc 명령어를 사용해서 type-check를 실행해보자

tsc hello.ts

Tada!

"tada"란 무엇인가? 우리는 타입스크립트를 실행했지만 타입 에러와 관련해서 콘솔에 아무런 리포트를 받지 않았다.

대신 hello.js라는 새로운 파일이 생긴것을 확인할 수 있다. 이 파일은 hello.ts가 컴파일되면서 생성된 자바스크립트 파일이고 그 내용을 확인하면 .ts 파일이 무엇을 뱉어냈는지 알 수 있다.

// Greets the world.
console.log("Hello world!");

해당 케이스는 정말 심플한 코드로 타입스크립트가 변환할 것이 거의 없으므로 자바스크립트로 작성한 것과 별반 다른게 없다. 타입스크립트 컴파일러는 사람이 작성한 것과 같이 클린하고 읽기 편한 코드를 작성하려고 노력한다. 반면 그것은 쉽지않은 일이다. 타입스크립트는 일관된 들여쓰기를 수행하고 코드가 여러줄 겹쳐있을때 이를 염두해두고 주석을 유지하고자 노력한다.

아래와 같은 코드를 다시 작성해보자.

// This is an industrial-grade general-purpose greeter function:
function greet(person, date) {
  console.log(`Hello ${person}, today is ${date}!`);
}
 
greet("Brendan");

만약 tsc hello.ts를 수행한다면 우리는 아래와 같은 에러 코드를 확인할 수 있다.

Expected 2 arguments, but got 1.

타입스크립트는 우리가 잊은 greet 함수의 아규먼트를 언급해준다. 타입스크립트는 여전히 표준 자바스크립트만 이용하여 type-check를 수행하여 코드의 문제를 찾아준다.

Emitting with Errors

마지막 코드에서 우리가 알아차리지 못한것은 hello.js 파일이 변경되었다는 것이다. 해당 파일을 열어보면 우리가 입력한 파일과 동일한 내용 입력되어 있는것을 확인할 수 있다. Typescript가 에러를 발생했는데도 불구하고 파일이 변경된 것에 조금 놀랄 수 있겠지만 이 방식은 Typescript의 코어 밸류중 하나이다.

다시 말하자면 type-cheker는 수용 가능한 종류를 구분하여 type-checking 코드가 우리가 실행하는 프로그램 종류를 제한하게 된다면, 대부분의 경우 문제가되지 않을 것이다. 반면 기존 잘 동작하던 JavaScript를 TypeScript로 마이그레이션 하는 과정에서 에러를 발견하게 된다고 가정해 보자. 결국 우리는 type-checker에 의해서 클린 코드에 도달하게 될 것이다 그러나 해당 원본 JavaScript 코드가 이미 문제없이 동작하고 있었다면 우리가 굳이 TypeScript로 변환하여 JavaScript의 실행을 중단할 필요가 있을까?

그래서 TypeScript는 그러한 동작 방식(에러가 발생한 경우 자바스크립트 파일을 컴파일하지 않는)으로 동작하지 않는다. 물론 시간이 지남에 따라 실수에 대해 더 방어적이고 엄격한 TypeScript의 동작을 원하는 경우가 생기게 될것이다. 이 경우 noEmitOnError를 컴파일러 옵션에 추가해 주면된다. 이후 hello.ts 파일을 변경하고 tsc 컴파일을 수행해보자

tsc --noEmitOnError hello.ts

hello.js 파일이 업데이트되지 않는 것을 확인할 수 있다.

Explicit Types

지금까지 우리는 TypeScript에 어떤게 person 이나 date 인지 알려주지 않았다. TypeScript에 이것을 알려주기 위해 각각 personstring 객체 이고 dateDate 객체라는 것으로 수정해 보자. Date 객체를 String으로 표현할때는 toDateString() 메서드를 이용하면 된다.

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

우리는 greet 함수가 호출될 때 사용되는 person, date 파라미터에 대한 타입 애노테이션을 추가했다. 이르루 통해 greet 함수 호출 시 personstring을, dateDate 객체를 사용한 다는 것을 알 수 있다.

이것과 함께, 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 타입을 리턴하기 때문이다. 반면 Date가 아닌 new Date()를 수행하면 우리가 예상했던 결과를 얻을 수 있다.

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
 
greet("Maddison", new Date());

항상 명확한 애노테이션(explicit annotation)을 사용해야하는 것은 아니다. 해당 애노테이션이 생략되더라도 대부분의 케이스에서 TypeScript는 타입을 추론할 수 있다.

TypeScript에게 msgstring 타입을 가졌다고 알려주지 않아도 그것을 찾아낸다. 이와 같이 타입 시스템이 타입을 찾을 수 있는 경우 별도의 애노테이션을 추가하지 않는것이 좋다.

Erased Types

위 예제에서 greet 함수가 tsc를 통해 컴파일된 JavaScript 파일에서 어떻게 변환되었는지 확인해보자

"use strict";
function greet(person, date) {
    console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));
}
greet("Maddison", new Date());

변환된 두가지 항목:
1. person, date에 작성한 애노테이션이 삭제됨
2. template string 작성을 위해 사용된 백틱(`)이 삭제되었고 concat을 사용한 plain string 값으로 변환됨

변환된 두가지 항목에 대한 상세 내용은 나중에 더 깊게 알아보도록 하자, 첫번째 변환된 항목에 집중해보면, 타입 애노테이션은 JavaScript(or ECMAScript to be pedantic)가 아니므로 컴파일되지 않은 TypeSciprt를 실행시킬 수 있는 브라우저나 다른 런타임은 존재하지 않는다. 그렇기때문에 TypeScript를 실행하기 위해서는 컴파일이 필수적으로 필요하다. (TypeScript를 실행할 수 있는 코드로 변환하기 위해 일부 TypeScript 코드를 삭제하거나 JavaScript 코드로 변환함)

Downleveling

위 예제에서 또 다른 하나는 template string이 수정되었다는 점이다.

`Hello ${person}, today is ${date.toDateString()}!`;

to

"Hello " + person + ", today is " + date.toDateString() + "!";

Template string은 ECMAScript 2015 버전의 기능중 하나이다. TypeScript는 신규 버전의 ECMAScript에서 부터 ECMASCript3(ES3) or ECMAScript5(ES5)와 같은 오래된 버전의 코드로 재작성할 수 있다. ECMAScript의 상위 버전에서 하위 버전으로 이동하는 것을 Downleveling이라고 부른다.

기본적으로 TypeScript는 매우 오래된 버전인 ES3의 코드로 JavaScript 코드를 작성한다. 이러한 버전의 선택은 target 옵션을 이용하여 변경할 수 있다. --target es2015 옵션을 이용하면 TypeScript는 ECMAScript 2015 버전으로 JavaScript 코드를 작성한다. 이것은 해당 코드를 실행하기 위해 브라우저나 런타임 환경이 ECMAScript 2015를 지원해야 한다는 의미다. tsc --target es2015 hello.ts 수행 후 생성된 hello.js 파일은 아래와 같다.

function greet(person, date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", new Date());

TypeScript는 기본적으로 ES3를 사용하지만 대부분의 브라우저에서 ES2015를 지원한다. 그러므로 오래된 특정 브루아저와의 호환성이 중요하지 않다면 대부분의 개발자들은 안전하게 ES2015 혹은 상위 버전을 사용해도 된다.

Strictness

TypeScript를 찾는 유저들은 type-cheker를 통해 서로 다른 목적을 달성하기 윈한다. 어떤 사람들은 오직 프로그램의 일부에 대한 검증을 도와주는 조금 더 루즈한 옵트인 성격의 기능을 원한다 그리고 그것은 여전히 존재한다. 타입 체크가 옵셔널하며 매우 러프한 타입 추론 그리고 null/undefined 값에 대한 확인을 하지 않는것이 기본 경험이 되는 TypeScript이다. tsc가 에러를 보여주는 방식과 매우 유사하게 기본적으로 방해가 되지 않도록 설정된다. 만약 이미 존재하는 JavaScript 파일을 마이그레이션할 때 이것은 이상적인 첫 스텝이 될 것이다.

반면, 많은 유저들이 가능하면 많이 바로 검증 가능한 TypeScript를 사용하길 원한다. 이게 바로 이 언어에서 Strictness 세팅을 제공하는 이유이다. Strictness 세팅은 스위치로부터 다이얼에 더 가까운것으로 type-chekcing을 전환한다. 다이얼을 더 많이 올릴수록 TypeScript는 더많이 확인하게 된다. 이것은 추가 작업을 요구할 수 있지만, 보편적으로 이것은 장기적 의미에서 가치가 있으며 더 철저한 검사와 정확한 툴링을 가능하게 한다. 가능한 경우 새 코드베이스에서는 항상 strictness 검사를 켜야한다.

TypeScript에서는 끄고 킬수 있는 몇개의 type-checking stricness 플래그를 제공하며 별도로 명시하지 않는한 활성화된 상태로 작성된다. strict 플래그는 CLI 혹은 tsconfig.json의 "strict": true에 모두 동시에 적용된다. 그리고 우리는 이러한 옵션을 각각 제외할 수 있다. 크게 투가지 옵션이 존재하는데 noImplictAnystrictNullChecks 이다.

noImplicitAny

어떤 곳에서는 TypeScript가 유형을 추론하지 않고 대신 매우 관대한 타입인 any로 취급하는 것이다. 이것은 최악의 상황이 아니다. 단지 JavaScript로 돌아가는 것이다.

어쨋거나, any를 자주 사용하면 애초에 TypeScript의 목적을 달성할 수 없다. 타입 체크가 잘된 프로그램은 더 검증되고, 툴링되어 결국 더 적은 버그가 발생될 것이다. noImplictitAny 옵션을 키게되면 암시적으로 any로 추론된 모든 변수에서 오류가 발생될 것이다.

strictNullChecks

기본적으로, nullundefined와 같은 변수는 모든 다른 타입에 할당할 수 있다. 이것은 코드를 더 쉽게 작성할 수 있게 되지만 셀수 없이 많은 버그를 만드는 null이나 undefined를 처리하는 것을 잊게 만든다. StrictNullChecks 플래그는 nullundefined를 더 명확하게 통제하고 우리가 nullundefined를 처리하는 것을 잊었는지 걱정하지 않아도 된다.

Null References: The Billon Dollar Mistake - Tony Hoare :
https://www.youtube.com/watch?v=ybrQvs4x0Ps

Everyday Type


이번 챕터에서는 가장 기본적인 JavaScript 타입을 알아보고 해당 타입들이 TypeScript에서 어떻게 동작하는지 설명할 것이다. 이번 챕터는 완벽한 리스트가 아니다. 추후 챕터에서 더 상세한 설명과 사용 방법에 대해서 설명할 예정이다.

The primitives: string, number, and boolean

JavaScript는 string, number, boolean 세가지의 가장 기본적인 원시 데이터형이 존재한다. TypeScript에서는 각각의 데이터형에 맞는 타입이 존재한다. 예상했듯이 우리가 JavaScript 내에서 typeof 연산자 사용할 경우 동일한 이름을 확인할 수 있다.

  • string은 "Hello world"와 같은 string 값을 표현한다.
  • number42와 같은 숫자를 의미한다. JavaScript는 integer와 같은 특별한 런타임 값이 존재하지 않기 때문에 이것은 int, float와 동등한 의미를 가진다.
  • booleantruefalse 두개의 값을 가지는 값이다.

Arrays

[1, 2, 3] 같은 배열을 정의하기 위해 number[] 문법을 사용할 수 있다. 이 문법은 모든 타입에 사용 가능하다(e.g. string[] 은 string 값의 배열을 의미한다.). 또한 Array<number>와 같은 유형도 마주칠 수 있는데 이것 또한 number[]와 동일한 의미를 갖는다. 추후 T<U>와 같은 제네릭 개념을 배우게 될것이다.

any

TypeScript는 any라는 특별한 타입을 가지고 있다. 이 타입은 특정 값에 대해서 typechecking error를 발생시키고 싶지 않을 때 사용하게 된다.

값의 타입이 any일 때 해당 값은 어떤 타입이든 관계없이 지정 가능하다. 함수를 지정하거나, 모든 타입의 값을 지정하거나 그 외 거의 모든게 가능하고 문법적으로 문제가 되지 않는다.

let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed 
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;

any 타입은 TypeScipt에게 특정 코드 라인이 문제없다는 것을 알려주기 위해 긴 코드를 작성해야할 경우 유용하게 사용된다.

noImplicitAny
특정 타입을 정하지 않고 TypeScript가 해당 타입을 유추하지 못할 경우 TypeScript는 해당 타입을 any로 지정한다.

보통 위와 같은 상황을 피하고 싶을 것이다. 왜냐하면 any 타입 자체는 type-check가 되지 않기 때문이다. 이따 noImplicitAny 플래그를 사용할 경우 any 타입이 발견될 때 에러를 발생시킬 것이다.

Type Annotations on Variables

const, var, let과 같은 변수를 생성할 때 변수의 타입을 명확하게 지정하기 위해 옵셔널하게 애노테이션을 추가할 수 있다.

let myName: string = "Alice";

대부분의 케이스에서 이것은 불필요하다. TypeScript가 해당 타입을 유추하기 때문이다. 예를 들어 아래 변수는 해당 타입의 이니셜라이저에 기반하여 타입이 유추된다.

// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";

Functions

JavaScript에서 함수는 데이터가 통과하는 것이다. TypeScipt는 함수의 input, output 값을 지정할 수 있도록 한다.

Parameter Type Annotations

함수를 지정할 어떤 타입의 매개변수가 허용되는지 지정하기 위해 각 매개변수 뒤에 타입 애노테이션을 추가할 수 있다.

// Parameter type annotation
function greet(name: string) {
  console.log("Hello, " + name.toUpperCase() + "!!");
}

매개변수가 타입 애노테이션을 가지고 있다면 해당 함수로 전달되는 인수는 타입 체크를 수행할 것이다.

// Would be a runtime error if executed!
greet(42);
Argument of type 'number' is not assignable to parameter of type 'string'.

Even if you don’t have type annotations on your parameters, TypeScript will still check that you passed the right number of arguments.

Object Types

원시 타입과 별개로 우리가 마주칠 가장 일반적인 타입은 오브젝트 타입이다. 이 타입은 JavaScript의 속성과 함께 거의 모든 값을 참조한다.

// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

두개의 속성 타입에 대한 애노테이션을 추가했다. x, y 각각 number를 의미한다. 타입을 지정하는 것은 여전히 선택사항이다. 그러나 타입을 지정하지 않을 경우 TypeScript는 해당 타입을 any로 설정할 것이다.

Optional Properties

오브젝트 타입은 매개변수의 전체 혹은 일부를 선택사항으로 지정할 수 있다. ? 문자를 추가하여 쉽게 설정 가능하다.

function printName(obj: { first: string; last?: string }) {
  // ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

JavaScript에서 존재하지 않는 속성에 접근할 경우 에러를 발생시키지 않고 undefined를 리턴할 것이다. 이러한 특성 때문에 선택 매개변수를 이용하기 전에 해당 변수가 undefined인지 확인해야 한다.

function printName(obj: { first: string; last?: string }) {
  // Error - might crash if 'obj.last' wasn't provided!
  console.log(obj.last.toUpperCase());
Object is possibly 'undefined'.
  if (obj.last !== undefined) {
    // OK
    console.log(obj.last.toUpperCase());
  }
 
  // A safe alternative using modern JavaScript syntax:
  console.log(obj.last?.toUpperCase());
}

Union Types

Defining a Union Type

유니온 타입은 2개 이상의 타입을 결합하여 체크하는 방식이다.

function printId(id: number | string) {
  console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.

Working With Union Types

TypScript는 유니온 타입의 모든 멤버가 유효할 경우 해당 타입을 허용해 줄것이다. 예를들어 string | number에서 string에만 존재하는 함수를 사용할 수 없다.

function printId(id: number | string) {
  console.log(id.toUpperCase());
}
Property 'toUpperCase' does not exist on type 'string | number'.
  Property 'toUpperCase' does not exist on type 'number'

위 코드는 아래와 같이 변경하여 사용 가능하다.

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

Array.isArray를 이용한 코드는 아래와 같다.

function welcomePeople(x: string[] | string) {
  if (Array.isArray(x)) {
    // Here: 'x' is 'string[]'
    console.log("Hello, " + x.join(" and "));
  } else {
    // Here: 'x' is 'string'
    console.log("Welcome lone traveler " + x);
  }
}

Type Aliases

위에서 타입 애노테이션을 이용하여 오브젝트 타입과 유니온 타입을 사용했다. 조금 더 편리한 방법으로 타입 앨리어스가 존재한다.

타입 앨리어스 구문은 아래와 같다.

type Point = {
  x: number;
  y: number;
};
 
// Exactly the same as the earlier example
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

타입 앨리어스에는 모든 유형을 지정할 수 있다. 예를들면 아래와 같이 유니온 타입도 지정 가능하다.

type ID = number | string;

Interfaces

오브젝트 타입의 이름을 정하는 다른 방식을 인터페이스 정의라고 한다.

interface Point {
  x: number;
  y: number;
}
 
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

Differences Between Type Aliases and Interfaces

타입 앨리어스와 인터페이스는 매우 유사하다. 우리는 다양한 케이스에서 둘중 하나를 골라서 사용하면 된다. 두가지 중 가장 큰 차이점은 타입 앨리어스는 새로운 속성을 추가하는게 불가능하고 인터페이스는 언제든지 추가 가능하다는 점이다.

Narrowing

padLeft라는 함수가 있다고 가정해보자

function padLeft(padding: number | string, input: string): string {
  throw new Error("Not implemented yet!");
}

만약 paddingnumber라면 input 앞에 붙는 공백의 숫자로 처리할 것이다. 만약 paddingstring인 경우 inputpadding을 추가하기만 하면 된다.

function padLeft(padding: number | string, input: string) {
  return " ".repeat(padding) + input;
}
Argument of type 'string | number' is not assignable to parameter of type 'number'.
  Type 'string' is not assignable to type 'number'.

padding에서 에러가 발생한다. TypeScript는 number | string 유니온 타입에 number를 인자로 받을 경우 우리가 원하는 값을 얻을 수 없다고 경고하고 있다. 다른 의미로 padding은 첫번째로 number인지 확인하지 않았고 문자열인 경우 처리하지 않았기 때문에 아래와 같이 수정할 수 있다.

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

if 체크에서 TypeScript는 typeof padding === "number"를 type guard라는 특별한 형태의 코드로 이해한다. 이러한 비교를 통해 변수의 타입을 더 작은 범위로 좁혀주는 것을 Narrowing이라고 한다.

typeof type guards

JavaScript는 우리가 런타임에 가지고있는 변수에 대한 가장 기본적인 정보를 제공해주는 typeof 연산자를 지원한다. TypeScript에서 typeof 연산자의 반환값을 아래와 같은 문자열 세트이다.

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

padLeft에서 보았듯이 이 연산자는 JavaScript 라이브러리에 자주 등장하며 TypeScriptsms 이를 Narrowing 하기 위해 사용한다.

0개의 댓글