📘
til ) TypeScript 복습하기 02
June 03, 2024
Everyday Types
- 타입은 단지 타입 표기 외에 더 다양한 위치에 나타날 수 있고, 새로운 구조체를 만들고자 할 때 타입을 참조하는 경우도 있다.
원시 타입 : string, number, boolean
- String, Number, Boolean 과 같은 타입은 유효한 타입이긴 하지만, 특수 내장 타입으로 쓰면 안된다. 쓰지 말고, string, number, boolean을 사용해라.
배열
string[]
=Array<string>
[number]
는 튜플 타입으로 배열이 아니다.
any
- 특정 값으로 인하여 타입 검사 오류가 발생하는 것을 원하지 않을 때 사용할 수 있다.
let obj: any = { x: 0 };
// 아래 이어지는 코드들은 모두 오류 없이 정상적으로 실행됩니다.
// `any`를 사용하면 추가적인 타입 검사가 비활성화되며,
// 당신이 TypeScript보다 상황을 더 잘 이해하고 있다고 가정합니다.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
- 해당 타입은 코드상 특정 라인에 문제가 없다고 TS를 안심시킨다는 목적 단지 하나 때문에 긴 타입을 새로 정의하고 싶지 않을 때 유용하게 사용할 수 있다.
noImplicitAny
: 컴파일 플래그로 any 를 쓰는 것을 강제로 막을 수 있다.
변수에 대한 타입 표기
let myName: string = "Alice";
// 타입 표기가 필요하지 않습니다. 'myName'은 'string' 타입으로 추론됩니다.
let myName = "Alice";
- 추론 규칙을 명시적으로 학습하지 않아도 되고, 처음 시작 시에는 굳이 타입 표기를 적게 사용할 수 도 있다.
- 하지만 사용성, 프로젝트의 상황에 맞춰 타입 표기의 기준을 정하기만 하면 된다고 볼 수 있다.
함수
매개변수 타입 표기
// 매개변수 타입 표기
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
// 만약 실행되면 런타임 오류가 발생하게 됩니다!
greet(42);
//Argument of type 'number' is not assignable to parameter of type 'string'.
- 매개변수에 타입을 표기하지 않더라도, TS는 해당 인자의 전달에서 타입을 검사한다.
반환 타입 표기
function getFavoriteNumber(): number {
return 26;
}
- 반환타입은 표기하지 않아도 된다. TS가 이에 대해 return 문을 바탕으로 추론하여 전달해주기 때문이다.
- 그러나 코드의 잘못된 수정을 미연에 방지, 명시적 타입을 통해 정확한 전달 등을 위해 타입을 명시할 수 있다.
익명 함수
- TS는 해당 함수의 매개변수에 자동으로 타입을 부여한다.
// 아래 코드에는 타입 표기가 전혀 없지만, 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는 타입이 표기되지 않았음에도, 내부 내용을 통해 타입을 추론하며, s의 타입을 알아내기 위해 병ㄹ의 추론된 타입과 더불어, forEach 함수의 타입을 활용했다.
- 이러한 과정을
문맥적 타입 부여
라고 부른다.
객체 타입
- 원시타입을 제외하고 가장 많이 마주치는 타입이 객체 타입이다.
- 객체는 프로퍼티를 가지는 JS 값을 말하며, 대부분이 이에 해당한다.
// 매개 변수의 타입은 객체로 표기되고 있습니다.
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 });
- 각 프로퍼티의 구분은
,
,;
를 사용할 수 있다. 마지막에 붙이는 것은 선택사항이다. - 타입을 지정하지 않는다면 프로퍼티는 기본적으로
any
타입으로 간주한다.
옵셔널 프로퍼티
- 객체 타입에서 일부 혹은 모든 프로퍼티 타입을 선택적으로 지정할 수 있다.
?
라고 적으면 옵셔널로 지정된다.
function printName(obj: { first: string; last?: string }) {
// ...
}
// 둘 다 OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
- JS에서는 존재하지 않는 프로퍼티에 접근하였을 때, 런타임 오류가 발생하지 않고, undefined 값을 얻게 된다. 이 때문에 옵셔널 프로퍼티 읽었을 때, 해당 값을 사용하기 앞서, undefined 여부를 확인해야 한다.
function printName(obj: { first: string; last?: string }) {
// 오류 - `obj.last`의 값이 제공되지 않는다면 프로그램이 멈추게 됩니다!
console.log(obj.last.toUpperCase());
//'obj.last' is possibly 'undefined'.
if (obj.last !== undefined) {
// OK
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 });
//Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
유니언 타입 사용하기
- 유니언 타입의 멤버 중 하나에 해당하는 타입을 제공하면 문제없이 동작한다.
- 단, 유니언이 되면서 사용되는 경우, 특정 하나의 타입에서만 사용되는 기능은 사용이 불가능하다.
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'.
}
- 위의 케이스를 해결하기 위해서는
typeof
를 통해 타입에 대한 명확함이 확보 되었을 때 쓰도록 만드는 것이다.
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);
}
}
- 위의 케이스와 반대 경우도 있어서, 둘다 공통의 메서드, 프로퍼티를 가지는 경우 둘다 사용이 가능하다.
// 반환 타입은 'number[] | string'으로 추론됩니다
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
타입 별칭
- 객체 타입, 유니언 타입을 사용할 때 해당 타입을 표기하는 방식을 썼지만, 똑같은 타입을 재 사용하거나 다른 이름으로 지정해 부를 수도 있다. 이 때 사용하는 도구다
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;
- 한 가지 기억할 사항은 타입별칭 개념은 별칭을 지정한다는 것으로 끝나는 것이며, 동일 타입에 대해 여러 버전을 만드는 것이 아니다. 따라서 타입에 대한 베타성을 확보하지 않기 때문에 예시 코드 같은 케이스도 허락된다.
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 });
타입 별칭과 인터페이스의 차이점
- 두 개념은 유사하고, 자유롭게 어느것으로든 만들어 사용하는 것이 가능하다.
- 그러나 이 두 개념의 차이는 타입은 새 프로퍼티를 추가하도록 개방될수 없고, 인터페이스는 확장이 가능하다.
인터페이스 확장하기
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'.
- 대부분의 경우 타입과 인터페이스 중 기호에 따라 선택이 가능하다. 하지만 인터페이스의 경우 객체의 모양을 선언하는데 사용되고, 기존 원시 타입에 별칭을 부여하는 등에는 type 만 쓸 수 있다는 점을 인지하고 있을 것.
- 일단 모르겠다 싶으면
interface
를 사용하고, 이후 문제가 발생한다면type
을 쓰길 권장한다.
타입 단언
- 어떤 경우 TS보다 더 정확하게 타입에 대한 정보를 아는 경우가 있을 수 있다. 이때 타입 단언을 통해 선언을 구체적으로 명시해줄 수 있다.
// 전형적인 타입 단언
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
// 꺽쇠를 활용한 타입 단언
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
- 이러한 기능은 컴파일러에 의해 컴파일 될 시에는 제거 되고, 런타임에 영향을 주진 않는다.
- 이 규칙이 지나치게 보수적으로 작용하여, 강제 변환이 허용되지 않을 수 있다. 이 경우 두번 단언을 사용하여 any 또는 unknown 으로 우선 변환 한 뒤 원하는 타입으로 변환할 수도 있다.
const a = (expr as any) as T;
리터럴 타입
- JS 부터 var, let 은 모두 변수가 저장 가능하며, 변경이 가능하다. 이에 비해 const 는 값의 변경이 불가능하다.
- TS에서는 리터럴 값을 위한 타입을 생성하는 방식에 이러한 변수 선언 방식이 그대로 적용된다.
let changingString = "Hello World";
changingString = "Olá Mundo";
// 변수 `changingString`은 어떤 문자열이든 모두 나타낼 수 있으며,
// 이는 TypeScript의 타입 시스템에서 문자열 타입 변수를 다루는 방식과 동일합니다.
const constantString = "Hello World";
// 변수 `constantString`은 오직 단 한 종류의 문자열만 나타낼 수 있으며,
// 이는 리터럴 타입의 표현 방식입니다.
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" | 9"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"'.
리터럴 추론
- 기본적으로 객체를 사용하여 변수를 초기화하면 TS 는 해당 객체의 프로퍼티는 값이 변경 가능하다고 가정한다.
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"'.
- 위의 예시는 string으로 추론되는 값을 넣었으나, “GET” 으로 추론되지 않아 에러가 발생했다고 오류가 나고 있다. 이러한 경우 해결 방법은 두 가지 존재한다.
- 특정 타입 단언을 통해 추론 방식을 변경한다.
// 수정 1: const req = { url: "https://example.com", method: "GET" as "GET" }; // 수정 2 handleRequest(req.url, req.method as "GET");
- 이 경우 수정의 방법이 두가지로 나오는데, 1번의 경우 애초에 값이 “GET” 타입이다 라는 점을 강조하는 것이며, 수정 2의 경우 req.method 라는 값이 어쨌든 “GET” 타입을 가질 것이라는 사실을 강조하는 형태이다.
as const
를 사용하여 객체 전체를 리터럴 타입으로 변환할 수 있다.ts const req = { url: "https://example.com", method: "GET" } as const; handleRequest(req.url, req.method);
- 특정 타입 단언을 통해 추론 방식을 변경한다.
null 과 undefined
- JS에서부터 유구한 전통으로 값이 없음을 나타내는 원시 값이다.
- TS 에서는 각 값에 대응하는 동일한 이름의 두 가지 타입이 존재하는데, 이는 옵션 설정에 따라 달라진다.
strictNullChecks 가 설정되지 않았을 때
- 해당 옵션이 설정 되지 않으면, 어떤 값이 null, undefined 일 때 그 값을 접근이 가능하며, 모든 타입의 변수에 대입 될 수 있다.
- 기존의 다른 언어들과 차이 없는 접근 방식이라 볼 수 있으며, 이는 버그를 발생시킬 수 있기에 옵션을 켜는 것이 권장된다.
strictNullChecks 가 설정되어 있을 때
- 해당 옵션이 설정되면, null, undefined 값을 사용하는 경우에는 무조건 해당 값이 존재 여부를 검사하는 작업을 수행해야 한다.
function doSomething(x: string | undefined) {
if (x === undefined) {
// 아무 것도 하지 않는다
} else {
console.log("Hello, " + x.toUpperCase());
}
}
Null 아님 단언 연산자(접미사 !)
- 그러나 이러한 형태는 빡빡하고, 결코 null, undefined 가 나오지 않는 경우를 확실하게 표현할 수 있는데, 그것이 바로
!
표현이다. 단, 이는 반드시 해당 값이 아닐 때만 사용해야한다.
function liveDangerously(x?: number | undefined) {
// 오류 없음
console.log(x!.toFixed());
}
열거형
- 사용법을 명확하게 파악하지 않았다면, 가능하면 실 사용은 보류하는 것이 좋다.
- 해당 문서 링크
자주 사용되지 않는 원시형 타입
- JS 에서 원시 타입으로 추가는 되었으나 안 쓰이는 타입 일부를 소개한다.
- bigint : ES 2020 이후 큰 정수를 다루기 위한 타입
// BigInt 함수를 통하여 bigint 값을 생성
const oneHundred: bigint = BigInt(100);
// 리터럴 구문을 통하여 bigint 값을 생성
const anotherHundred: bigint = 100n;
- symbol : 전역적으로 고유한 참조값을 생성하는데 사용할 수 있는 원시 타입이다.
const firstName = Symbol("name"); //
const secondName = Symbol("name"); // 값은 서로 같더라도, 그 자체가 고유 객체로 취급된다.
if (firstName === secondName) {
//This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.
// 절대로 일어날 수 없습니다
}