타입스크립트 핸드북 훑어보기 [2. Everyday Types]
들어가며
타입스크립트 핸드북 훑어보기 [1. The Basics]
들어가며 이어서 타입스크립트 문서 훑어보기 들어가며 새로 시작한 프로젝트에서 프론트엔드는 타입스크립트로의 개발이 확정되었다. 타입스크립트를 한번도 사용해보지 않았던지라, 레퍼런
woongtech.tistory.com
이번 장에서는 자바스크립트 코드에서 찾아볼 수 있는 가장 흔한 타입들을 다루고, 이 타입들을 타입스크립트에서 어떻게 기술하는지 각각의 대응방식에 대해 설명한다. 타입은 단지 타입 표기 이외에도 훨씬 다양한 위치에 나타날 수 있다. 그러기 위해서는 우선 자바스크립트와 타입스크립트 코드를 작성할 때 가장 기본적이면서 흔하게 볼 수 있는 타입을 다시 보는데에서 시작해보는 것이 좋다.
원시타입 : string, number, boolean
이 세 타입은 자바스크립트에서 가장 흔하게 사용되는 원시타입들이다.
- string은 "Hello, World!"와 같은 문자열 값을 나타낸다.
- number는 42와 같은 숫자를 나타낸다. 자바스크립트는 정수를 위한 런타임 값을 별도로 가지지 않으므로, int 또는 float같은 형태는 존재하지 않는다. 모든 수를 number로 표현할 수 있다.
- boolean은 true와 false라는 두 가지 값만을 가진다.
배열
[1, 2, 3]과 같은 배열의 타입을 지정할 때 number[] 구문을 사용할 수 있다. 이 구문은 모든 타입에서 사용할 수 있다.
any
타입스크립트는 any라고 불리는 특별한 타입을 가지고 있으며, 특정 값으로 인해 타입 검사 오류가 발생하는 것을 원하지 않을 때 사용할 수 있다. 어떤 값의 타입이 any이면, 해당 값에 대하여 임의의 속성에 접근할 수 있고, 함수인 것처럼 호출할 수 있으며, 다른 임의 타입의 값에 할당하는 등의 구문적으로 유효한 것들이면 무엇이든 할 수 있다.
let obj: any = { x: 0 };
// 아래 이어지는 코드들은 모두 오류 없이 정상적으로 실행된다.
// any를 사용하면 추가적인 타입 검사가 비활성화되며, 사용자가 타입스크립트보다 상황을 더 잘 이해하고 있다고 가정한다.
obj.foo();
obj.bar = 100;
obj = "Hello";
const n: number = obj;
any타입은 코드상의 특정 라인에 문제가 없다고 생각할 경우, 모든 타입에 대한 동작을 정의하지 않아도 될 때, 유용하게 사용할 수 있다.
변수에 대한 타입 표기
const, var, let 등을 사용해 변수를 선언할 때, 변수의 타입을 명시적으로 지정하기 위해 타입 표기를 추가할 수 있으며 이는 선택사항이다.
let myName: string = "Alice";
타입스크립트에서의 타입 표기는 항상 타입의 대상 뒤쪽에 위치한다. C++에서의 int x = 0;과 같은 표현은 허용되지 않는다.
대부분의 경우, 타입 표기는 필요하지 않다. 가능하다면 타입스크립트 내부에서 자동을 코드 내의 있는 타입들을 추론하고자 시도한다. 예를 들어 변수의 타입은 해당 변수의 초깃값의 타입을 바탕으로 추론된다.
함수
함수는 자바스크립트에서 데이터를 주고 받는 주요 수단이다. 타입스크립트에서는 함수의 입출력 타입을 지정할 수 있다.
매개변수 타입 표기
함수를 선언할 때, 함수가 허용할 매개변수 타입을 선언하기 위해 각 매개변수 뒤에 타입을 표기할 수 있다. 매개변수 타입은 매개변수 이름 뒤에 표기한다.
// 매개변수 타입 표기
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
매개변수에 타입이 표기되었다면, 해당 함수에 대한 인자는 검사가 이루어진다.
반환 타입 표기
매개변수 타입 표기와 마찬가지로, 반환 타입은 표기하지 않아도 되는것이 일반적이다. 왜냐하면 타입스크립트가 해당 함수에 들어있는 return 구문을 바탕으로 반환 타입을 추론할 것이기 때문이다. 위 예시에서 사용된 타입 표기는 큰 의미를 갖지 않는다. 때에 따라 문서화를 목적으로, 또는 코드의 잘못된 수정을 미연에 방지하고자, 혹은 지극히 개인적인 선호에 의해 명시적인 타입 표기를 수행하는 코드도 존재한다.
function getFavoriteNumber(): number {
return 26;
}
익명 함수
익명 함수는 함수 선언과 조금 다르다. 함수가 코드 상에서 위치한 곳을 보고 해당 함수가 어떻게 호출될지 알아낼 수 있다면, 타입스크립트는 해당 함수의 매개 변수에 자동으로 타입을 부여한다.
// 아래 코드에는 타입 표기가 전혀 없지만, TypeScript는 버그를 감지할 수 있습니다.
const names = ["Alice", "Bob", "Eve"];
// 함수에 대한 문맥적 타입 부여
names.forEach(function (s) {
console.log(s.toUppercase());
});
// 화살표 함수에도 문맥적 타입 부여는 적용됩니다
names.forEach((s) => {
console.log(s.toUppercase());
});
위 코드에서 매개변수 s는 타입이 표기되지 않았음에도 불구하고, 타입스크립트는 s의 타입을 알아내기 위해 배열의 추론된 타입과 더불어 forEach 함수의 타입을 활용했다. 이 과정을 문맥적 타입 부여라고 한다.
객체 타입
원시 타입을 제외하고 가장 많이 마주하는 타입이 객체 타입이다. 객체는 프로퍼티를 가지는 자바스크립트 값을 말하는데, 대부분의 경우가 이것에 해당한다. 예를 들어, 아래 함수는 좌표로 보이는 객체를 인자로 받고 있다.
// 매개 변수의 타입은 객체로 표기되고 있습니다.
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 타입이다. 각 프로퍼티를 구분할 때 , 또는 ; 를 사용할 수 있고, 가장 마지막에 위치한 구분자의 표기는 선택 사항이다.
옵셔널 프로퍼티
객체 타입의 일부 또는 모든 프로퍼티의 타입에 ?를 붙이면 옵셔널로 지정할 수 있다.
function printName(obj: { first: string; last?: string }) {
// ...
}
// 둘 다 OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
자바스크립트에서는 존재하지 않는 프로퍼티에 접근하는 경우 에러가 발생하지 않고 undefined 값을 얻게 된다. 이 때문에 옵셔널 프로퍼티를 읽었을 때, 해당 값을 사용하기에 앞서 undefined인지 여부를 확인해야 한다.
function printName(obj: { first: string; last?: string }) {
// 오류 - `obj.last`의 값이 제공되지 않는다면 프로그램이 멈추게 됩니다!
console.log(obj.last.toUpperCase());
// OK - undefined 여부 확인이 선행된다.
if (obj.last !== undefined) {
console.log(obj.last.toUpperCase());
}
// 최신 JavaScript 문법을 사용하였을 때 또 다른 안전한 코드
console.log(obj.last?.toUpperCase());
}
유니언 타입
타입스크립트의 시스템에서는 기존의 타입을 기반으로 다양한 연산자를 사용해 새로운 타입을 만들 수 있다.
유니언 타입 정의하기
타입을 조합하는 첫 번째 방법은 유니언 타입을 사용하는 것이다. 유니언타입은 서로 다른 두 개 이상의 타입들을 사용해 만드는 것으로, 유니언 타입의 값은 타입 조합에 사용된 타입 중 무엇이든 하나를 타입으로 가질 수 있다.
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// 오류
printId({ myID: 22342 });
유니언 타입 사용하기
유니언 타입을 다룰 때는 해당 유니언 타입의 모든 멤버에 대해 유효한 작업일 경우에만 허용된다. 예를들어 string | number라는 유니언 타입의 경우, string 타입에만 유효한 메서드는 사용할 수 없다.
이를 해결하기 위한 예시로 코드 상에서 유니언을 좁히는 방법이 있다. string | number라는 유니언 타입을 매개변수로 받는 함수가 있을 때, 함수 내부에서 string값이 들어올 경우에만 동작하도록 하는 방법으로 문제를 해결할 수 있다.
function printId(id: number | string) {
if (typeof id === "string") {
// 이 분기에서 id는 'string' 타입을 가집니다
console.log(id.toUpperCase());
} else {
// 여기에서 id는 'number' 타입을 가집니다
console.log(id);
}
}
또 다른 예시는 Array.isArray와 같은 함수를 사용하는 것이다.
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// 여기에서 'x'는 'string[]' 타입입니다
console.log("Hello, " + x.join(" and "));
} else {
// 여기에서 'x'는 'string' 타입입니다
console.log("Welcome lone traveler " + x);
}
}
타입 별칭
타입 별칭이란 똑같은 타입을 한 번 이상 재사용하거나 또 다른 이름으로 부르고 싶은 경우에 사용하게 된다.
type 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 });
타입 별칭을 사용하게 되면 객체 타입 뿐만아니라 모든 타입에 대해 새로운 이름을 부여할 수 있다. 예를 들어, 유니언 타입에서도 타입 별칭을 부여할 수 있다.
type ID = number | string;
타입 별칭은 "별칭"이라는 점에 유의해야 한다. 동일한 타입에 여러 별칭을 사용하는 것은 추천하지 않는 방식이다. 예를 들어, 다음 코드에서 string을 별칭을 붙여 사용할 경우에 대한 안좋은 예시를 설명한다.
type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}
// 보안 처리를 마친 입력을 생성
let userInput = sanitizeInput(getInput());
// 물론 새로운 문자열을 다시 대입할 수도 있습니다
userInput = "new input";
인터페이스
인터페이스 선언은 객체 타입을 만드는 또 다른 방법이다.
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 });
타입 별칭을 사용한 경우와 마찬가지로, 타입스크립트에서는 오직 printCoord에 전달된 값의 구조에만 관심을 가진다. 즉, 예측된 프로퍼티를 가졌는지의 여부만을 따지는 것이다.
그렇다면 타입 별칭과 인터페이스는 이토록 유사한데 왜 두 개를 구분지어 사용하는 것일까?
타입 별칭과 인터페이스의 차이점
이 둘은 매우 유사하며, 대부분의 경우 둘 중 하나를 자유롭게 선택해서 사용할 수 있다. 인터페이스의 대부분의 기능은 타입 별칭을 통해서도 동일하게 사용 가능하기 때문이다. 이 둘의 가장 핵심적인 차이점은 타입 별칭은 새 프로퍼티 추가를 할 수 없는 반면, 인터페이스의 경우 항상 확장될 수 있다는 점이 있다.
// 인터페이스 확장
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear = getBear()
bear.name
bear.honey
// 교집합을 통하여 타입 확장하기
type Animal = {
name: string
}
type Bear = Animal & {
honey: Boolean
}
const bear = getBear();
bear.name;
bear.honey;
// 기존의 인터페이스에 새 필드 추가
interface Window {
title: string
}
interface Window {
ts: TypeScriptAPI
}
const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
// 타입은 생성된 뒤에는 달라질 수 없다
type Window = {
title: string
}
type Window = {
ts: TypeScriptAPI
}
// Error: Duplicate identifier 'Window'.
대부분의 경우, 인터페이스를 먼저 사용하고 이후 문제가 발생하였을 때, 타입 별칭을 사용하는것을 추천하고 있다.
타입 단언
타입스크립트보다 사용자가 어떤 값에 대한 타입에 대한 정보를 더 잘 알고 있는 경우도 존재한다.
예를 들어 코드상에서 document.getElementById가 사용되는 경우, 타입스크립트는 HTMLElement 중에 무언가만 반환된다는 것만을 알 수 있는 반면, 사용자는 페이지 상에서 사용되는 ID로 언제나 HTMLCanvasElement가 반환된다는 사실을 이미 알고 있을 수 있다.
이 경우, 타입 단언을 사용하면 조금 더 구체적으로 명시할 수 있다.
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
위 두 코드는 동일한 동작을 수행하며, 타입 단언은 컴파일러에 의해 제거되고, 런타임 동작에는 영향을 주지 않는다. 따라서 타입 단언이 틀렸다고 하더라도 예외가 발생하거나 null이 생성되지 않을 것이다.
타입 단언에 의해 강제적으로 타입을 변환하고싶다면, string을 바로 number로 변환하는 방법 대신, any로 변환한 뒤, 원하는 타입으로 변환하는것을 제안하고 있다.
// error
const x = "hello" as number;
// ok
const x = ("hello" as any) as number;
리터럴 타입
string과 number와 같은 일반적인 타입 이외에도, 구체적인 문자열과 숫자 값을 타입 위치에서 지정할 수 있다. 자바스크립트에서 var와 let은 모두 변수에 저장 가능한 값의 종류를 변경할 수 있지만, const는 불가능하다. 이 특징들은 타입스크립트에서 리터럴 값을 위한 타입을 생성하는 방식에 그대로 사용된다.
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
// OK
printText("Hello, world", "left");
// error
printText("G'day, mate", "centre");
null & undefined
자바스크립트에는 빈 값 또는 초기화되지 않은 값을 가리키는 두 가지 원시값이 존재한다. 바로 null과 undefined이다.
타입스크립트에는 각 값에 대응하는 동일한 이름의 두 가지 타입이 존재하며, 각 타입의 동작 방식은 strictNullChecks 옵션의 설정 여부에 따라 달라진다.
- strictNullChecks 가 설정되지 않았을 때
strictNullChecks가 설정되지 않았다면, 어떤 값이 null 또는 undefined일 수 있더라도 해당 값에 평소와 같이 접근할 수 있으며, null과 undefined는 모든 타입의 변수에 대입될 수 있다. 이는 Null 검사를 하지 않는 언어(C#, Java 등)의 동작 방식과 유사하다. Null 검사의 부재는 버그의 주요 원인이 되기도 한다. 별다른 이유가 없다면, 코드 전반에 걸쳐 strictNullChecks 옵션을 설정하는 것을 항상 권장하고 있다. - strictNullChecks 설정되었을 때
strictNullChecks가 설정되었다면, 어떤 값이 null 또는 undefined일 때, 해당 값과 함께 메서드 또는 프로퍼티를 사용하기에 앞서 해당 값을 테스트해야 한다. 옵셔널 프로퍼티를 사용하기에 앞서 undefined 여부를 검사하는 것과 마찬가지로, 좁히기를 통하여 null일 수 있는 값에 대한 검사를 수행할 수 있다.
function doSomething(x: string | undefined) {
if (x === undefined) {
// 아무 것도 하지 않는다
} else {
console.log("Hello, " + x.toUpperCase());
}
}
! 연산자
타입스크립트에서는 명시적인 검사를 하지 않고도 null과 undefined를 제거할 수 있는 특별한 구문을 제공한다. 표현식 뒤에 !를 작성하면 해당 값이 null 또는 undefined가 아니라고 타입을 단언하는 것이다.
function liveDangerously(x?: number | undefined) {
// 오류 없음
console.log(x!.toFixed());
}
다른 타입 단언과 마찬가지로 코드의 런타임 동작을 변화시키지 않기 때문에, ! 연산자는 반드시 해당 값이 null 혹은 undefined가 아닌 경우에만 사용해야 한다.
열거형
열거형은 타입스크립트의 추가기능으로, 어떤 값이 이름이 있는 상수 집합에 속한 값 중 하나일 수 있도록 제한하는 기능이다. 대부분의 타입스크립트 기능과 달리, 이 기능은 자바스크립트의 언어와 런타임 수준에 추가된다. 열거형은 이 하나로 분량이 충분하므로 열거형 문서를 읽는것을 추천한다.
Handbook - Enums
How TypeScript enums work
www.typescriptlang.org
자주 사용되지 않는 원시형 타입
- bigint
아주 큰 정수를 다루기 위한 원시 타입으로 자바스크립트에 추가되었다.
// BigInt 함수를 통하여 bigint 값을 생성
const oneHundred: bigint = BigInt(100);
// 리터럴 구문을 통하여 bigint 값을 생성
const anotherHundred: bigint = 100n;
- symbol
symbol은 전역적으로 고유한 참조값을 생성하는 데에 사용할 수 있는 원시 타입이며, Symbol() 함수를 통해 생성할 수 있다.
Documentation - Symbols
Using the JavaScript Symbol primitive in TypeScript
www.typescriptlang.org
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 절대로 일어날 수 없습니다
}