TypeScript 배우기 - 3. 일반 유형
이번 장에서는 JavaScript 코드의 가장 흔한 타입들을 다루고, 이 타입을 TypeScript에서는 어떻게 표현하는지 설명합니다. 우선 JavaScript 또는 TypeScript 코드에서 기본적이며 흔하게 만날 수 있는 타입을 살펴보는 것부터 시작해봅시다.
일반 유형
원시 타입 : string, number, boolean
원시 타입이란 객체가 아니면서 또한 메서드도 가지지 않는 데이터 타입을 말합니다. JavaScript는 이러한 원시 타입으로 string
, number
, boolean
이 있습니다.
string
은 "Hello, world!"`와 같은 문자열 값number
는42
와 같은 숫자. JavaScript는 정수와 실수를 구분하지 않으므로int
또는float
과 같은 것은 존재하지 않음boolean
은true
와false
라는 두가지 값을 가짐
배열
[1, 2, 3]과 같은 숫자 배열의 경우
number[]구문을 사용할 수 있습니다. 마찬가지로
string[]`은 문자열 배열을 의미합니다.
any
TypeScript의 any
는 어떠한 타입도 허용한다는 의미로 그러므로 정적 타입 검사를 수행할 수 없게 됩니다. 아래의 코드는 any
유형에 의해 컴파일 오류가 발생하지 않습니다.
let obj: any = { x: 0 };
// 아래 이어지는 코드들은 모두 오류 없이 정상적으로 실행됩니다.
// `any`를 사용하면 추가적인 타입 검사가 비활성화되며,
// 당신이 TypeScript보다 상황을 더 잘 이해하고 있다고 가정합니다.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
any
유형은 이미 잘 동작하는 JavaScript 코드를 TypeScript 코드로 전환할 때 유용합니다. 하지만 일반적인 경우에 any
유형을 사용하는 것은 정적 유형 검사를 할 수 없게 되므로 'noImplicitAny플래그를 통해 암묵적으로 유형이
any`로 간주하는 모든 경우를 오류로 처리할 수 있습니다.
변수에 대한 타입 표기
const
, var
, let
을 사용하여 변수를 선언할 때 변수 타입을 명시적으로 지정하거나 TypeScript가 유추가 가능할 경우 생략할 수 있습니다.
let myName: string = "Alice";
// 타입 표기가 필요하지 않습니다. 'myName'은 'string' 타입으로 추론됩니다.
let myName = "Alice";
함수
함수는 JavaScript에서 데이터를 주고 받는 수단입니다. 이 표현은 핸드북에 있는 표현인데 매력적이네요! TypeScript는 함수로 전달하는 입력 및 반환되는 출력에 타입을 지정할 수 있습니다.
매개변수 타입
함수를 선언할 때 함수에 전달할 매개변수에 타입을 표기할 수 있습니다.
// 매개변수 타입 표기
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
매개변수 타입에 의해 다음의 코드는 오류가 됩니다.
declare function greet(name: string): void;
// ---셍략---
// 만약 실행되면 런타임 오류가 발생하게 됩니다!
greet(42);
~~
Argument of type 'number' is not assignable to parameter of type 'string'.
반환 타입 표기
TypeScript는 반환 값에 대한 타입도 표기합니다.
function getFavoriteNumber(): number {
return 26;
}
이것을 생략할 수 도 있는데 TypeScript는 타입을 유추할 수 있으면 let
, var
, const
에서의 타입 생략과 마찬가지로 생략이 가능합니다.
function getFavoriteNumber() {
return 26; // number이므로 반환 타입은 생략 가능하고 `number`로 유추함
}
익명 함수
익명함수에서의 타입은 추론이 가능하므로 생략됩니다.
// 아래 코드에는 타입 표기가 전혀 없지만, TypeScript는 버그를 감지할 수 있습니다.
const names = ["Alice", "Bob", "Eve"];
// 함수에 대한 문맥적 타입 부여
names.forEach(function (s) {
console.log(s.toUppercase());
~~~~~~~~~~~~~
Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
// 화살표 함수에도 문맥적 타입 부여는 적용됩니다
names.forEach((s) => {
console.log(s.toUppercase());
~~~~~~~~~~~~~
Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
여기서 s
는 문자열로 추론되므로 toUppercase
의 오타를 올바로 잡아 오류로 처리합니다.
객체 타입
원시 타입을 제외하고 가장 많이 사용되는 타입은 객체 타입 입니다. 객체는 JavaScript에서 속성으로 이루어진 것을 말하는데 { x: number; y: number }
등이 됩니다.
// 매개 변수의 타입은 객체로 표기되고 있습니다.
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 });
옵셔널 속성
모든 속성을 전달하지 않아도 될 때가 있죠. 그런 경우 name?
으로 속성명 뒤에 ?
을 두면 필수 속성이 아니라는 것을 표현할 수 있습니다.
function printName(obj: { first: string; last?: string }) {
// ...
}
// 둘 다 OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
JavaScript에서는 존재하지 않는 속성에 접근할 때 런타임 오류가 발생하지 않고 undefined
값을 얻게 되는데 그렇기 때문에 옵셔널 속성을 처리할 때는 해당 값을 사용하기 앞서서 undefined
인지를 확인해야 합니다.
function printName(obj: { first: string; last?: string }) {
// 오류 - `obj.last`의 값이 제공되지 않는다면 프로그램이 멈추게 됩니다!
console.log(obj.last.toUpperCase());
// Object is possibly 'undefined'.
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// 최신 JavaScript 문법을 사용하였을 때 또 다른 안전한 코드
console.log(obj.last?.toUpperCase());
}
유니언 타입
TypeScript의 타입 시스템은 기존 타입을 조합하여 새로운 타입으로 만들 수 있습니다.
유니언 타입 정의
타입을 조합하는 첫번째 방법은 유니언 타입을 사용하는 것입니다. 조합은 |
으로 하며 조합에 해당 타입을 모두 허용합니다. 이 때 허용하는 타입을 유니언 타입의 멤버
라고 합니다.
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// 오류
printId({ myID: 22342 });
// Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
printId
함수의 인자 id
는 number
또는 string
타입을 허용합니다. 그러므로 { myID: 22342 }
의 객체 타입은 오류가 발생합니다.
유니언 타입 사용
유니언 타입을 사용하게 되면 유니언 타입 멤버가 공통으로 제공하는 값, 속성, 함수일 경우만 오류가 발생하지 않습니다.
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'.
}
위의 코드의 경우 toUpperCase
함수는 string
타입에만 사용할 수 있는 함수로 number
에서는 제공하지 않으므로 number | string
유니온 타입으로 호출할 수 없는 함수가 됩니다.
이를 해결하려면 typeof
를 이용해 제공하는 변수의 타입이 무엇인지 분기해야 합니다. 이 방법은 JavaScript의 그것과 동일합니다.
function printId(id: number | string) {
if (typeof id === "string") {
// 이 분기에서 id는 'string' 타입을 가집니다
console.log(id.toUpperCase());
} else {
// 여기에서 id는 'number' 타입을 가집니다
console.log(id);
}
}
또다른 예시는 string[] | string
유니온 타입일 경우 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);
}
}
유니온 타입 멤버들이 모두 가지고 있는 속성이나 함수 호출은 허용됩니다.
// 반환 타입은 'number[] | string'으로 추론됩니다
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
위의 코드의 경우 number[]
와 string
모두 slice
함수를 가지므로 오류 없는 정상 코드입니다.
타입 별칭
위의 코드들은 별도의 타입 별칭을 사용하지 않는 코드 였습니다. 하지만 실제로 코드를 작성할 때는 반복되는 유형을 타입 별칭으로 정의해서 사용하는 것이 편합니다.
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 });
위의 코드는 { x: numner; y: number }
로 별칭(alias) 하였습니다. 이후 별칭된 이름으로 그 타입을 사용할 수 있습니다.
또한 다음의 유니언 타입 또한 타입 별칭으로 사용할 수 있습니다.
type ID = number | string;
타입 별칭은 완전한 새로운 타입을 만드는 것이 아닙니다. 다음의 코드는 다른 타입 별칭이지만 오류가 발생하지 않습니다. 타입 별칭이 의미하는 타입이 동일하게 string
이기 때문입니다.
declare function getInput(): string;
declare function sanitize(str: 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
함수는 매개변수로 받는 pt
가 Point
인터페이스와 맞게 전달이 된다면 정상 코드로 해석하며, 인터페이스에 정의된 방식 -- 여기서는 x
라는 number
속성과 y
라는 number
속성 -- 에만 부합하면 동작합니다. 이렇게 타입이 가지는 구조와 능력에만 관심을 가진다는 점에서 TypeScript는 구조적 타입 시스템이라고 불립니다.
타입 별칭과 인터페이스의 차이점
그렇다면 매우 유사해 보이는 타입 별칭과 인터페이스와는 어떤 차이점이 있을까요? 대표적인 차이점은 interface
는 새 속성을 추가할 수 있지만 type
은 불가능 하다는 점입니다.
타입 단언
TypeScript가 파악하고 있는 타입보다 좀 더 명확한 타입을 알고 있을 경우 as
를 사용해서 코드로 표현할 수 있습니다.
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
타입 표기와 마찬가지로 타입 단언 표현은 컴파일러에 의해 제거됩니다. 코드가 .tsx
가 아닌 경우 꺽쇠 괄호를 사용하는 방법도 가능합니다.
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
하지만 TypeScript에서는 타입 단언에 의해 변환되는 타입을 보다 구체적인
또는 덜 구체적인
의미를 가지는 타입 (다른 표현으로 상속관계)만 허용합니다. 다음의 코드 처럼 string
을 number
로 변환하는 타입 단언은 허용하지 않습니다.
const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
이런 특징 떄문에 유효할 수 있는 강제 변환이 허용되지 않을 수 도 있습니다. 이런 경우 아래의 코드 처럼 any
로 변환한 뒤 다시한번 변환하는 것으로 두번의 단언으로 가능합니다.
declare const expr: any;
type T = { a: 1; b: 2; c: 3 };
// ---중간 생략---
const a = (expr as any) as T;
리터럴 타입
TypeScript에서는 구체적인 문자열 또는 숫자를 타입으로 표현할 수 있습니다.
let changingString = "Hello World";
changingString = "Olá Mundo";
// 변수 `changingString`은 어떤 문자열이든 모두 나타낼 수 있으며,
// 이는 TypeScript의 타입 시스템에서 문자열 타입 변수를 다루는 방식과 동일합니다.
changingString;
// let changingString: string
let changingString: string
const constantString = "Hello World";
// 변수 `constantString`은 오직 단 한 종류의 문자열만 나타낼 수 있으며,
// 이는 리터럴 타입의 표현 방식입니다.
constantString;
// const constantString: "Hello World"
이를 리터럴 타입이라고 합니다.
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
Type '"howdy"' is not assignable to type '"hello"'.
하지만 하나의 리터럴 타입만 허용하는 것은 거의 의미가 없죠. 그렇기 때문에 대부분 유니언과 함께 사용되게 됩니다.
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
숫자 리터럴 또한 리터럴 타입으로 유니언과 함께 사용될 수 있습니다.
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
유니언에 의해 리터럴이 아닌 타입과도 함께 사용할 수 있습니다.
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
~~~~~~~~~~~
Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
boolean
또한 리터럴 타입으로 표현할 수 있는데 사실 boolean
는 true | false
유니언 타입의 별칭이라 할 수 있습니다. 매우 강력한 타입 시스템이네요!
리터럴 추론
TypeScript에서는 변수가 초기화 되면 해당 객체의 속성은 변화할 수 있다고 가정합니다.
declare const someCondition: boolean;
// ---중간 생략---
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
동일한 사항이 문자열에도 적용됩니다.
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
위의 예시에서 req.method
는 string
이고 코드의 어느 지점에서 GUESS
등으로 바뀔 수도 있으므로 handleRequest
함수의 두번째 인자에서 요구하는 "GET" | "POST"
유닌온 타입과 다르다고 평가하여 오류가 발생합니다.
이를 해결하려면 다음처럼 처리할 수 있습니다.
// 수정 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// 수정 2
handleRequest(req.url, req.method as "GET");
또는 as const
를 이용해서 전체를 리터럴로 처리할 수 있습니다.
declare function handleRequest(url: string, method: "GET" | "POST"): void;
// ---중간 생략---
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
as const
는 일반적인 const
와 유사하게 작동하는데 해당 객체의 모든 속성에 string
또는 number
와 같은 보다 일반적인 타입이 아닌 리터럴 타입의 값이 사용되도록 보장합니다.
null
과 undefined
JavaScript에서는 빈값 null
과 초기화 되지 않는 값 undefined
의 원시 값이 존재합니다.
TypeScript에서는 각 값에 대응하는 두가지 타입으로 존재합니다.
strictNullChecks
플래그 미설정
strictNullChecks
플래그가 설정되지 않았으면 모든 타입에 null
또는 undefined
가 대입될 수 있습니다. 하지만 이는 대부분 버그로 이어질 수 있으므로 strictNullChecks
플래그를 활성화 하는 것을 권장합니다.
strictNullChecks
플래그 설정
strictNullChecks
플래그가 설정되었아면 null
과 undefined
는 타입으로 간주합니다.
function doSomething(x: string | undefined) {
if (x === undefined) {
// 아무 것도 하지 않는다
} else {
console.log("Hello, " + x.toUpperCase());
}
}
Null이 아님 단언 연산자 (접미사 !)
TypeScript에서는 null
또는 undefined
가 아닐 경우 !
접미사를 통해 검사를 하지 않도록 할 수 있습니다.
function liveDangerously(x?: number | undefined) {
// 오류 없음
console.log(x!.toFixed());
}
하지만 !
접미사를 사용하면 검사를 수행하지 않으므로 해당 값이 반드시 null
또는 undefined
가 아닌 경우에만 사용해야 합니다.
열거형
열거형은 JavaScript에서 제공하는 것이 아니라 TypeScript에서 언어와 런타임 수준에서 추가하는 기능입니다. 열거형은 Enums에서 자세히 다룹니다.
bigint
ES2020 이후 아주 큰 정수를 다루기 위한 bigint
라는 원시 타입이 JavaScript에 추가되었습니다.
// BigInt 함수를 통하여 bigint 값을 생성
const oneHundred: bigint = BigInt(100);
// 리터럴 구문을 통하여 bigint 값을 생성
const anotherHundred: bigint = 100n;
symbol
symbol
은 전역적으로 고유한 참조값을 생성하는데 사용할 수 있는 원시 타입이며, Symbol()
함수를 통해 생성할 수 있습니다.
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
// 절대로 일어날 수 없습니다
}
정리
오늘은 TypeScript의 일반 타입에 대해 알아보았습니다. 다음 시간에는 핸드북의 Narrowing
에 대해 살펴보겠습니다.