
TypeScript 공식 Handbook을 읽은 후 작성하는 글 입니다.
TypeScript를 공부하면서 얻은 지식과 기초부터 탄탄하게 하기 위해 TypeScript Handbook의 내용을 다시 한번 정리하여 블로그에 문서화하기로 하였다.
아주 기본적인 TypeScript 타입에 대해서는 TypeScript Handbook에 친절하게 잘 설명해주고 있어서 추가적인 개인 학습을 위해 작성하는 것이고 공부를 하면서 계속 알게된 내용을 정리하고 TypeScript를 사용한 프로젝트를 진행해볼 계획이다.
React native도 공부할 계획이며 기회가 된다면 React native와 TypeScript를 같이 사용하여 프로젝트를 진행해보고 싶다.
TypeScript를 공부하기 앞서 왜 사용하는지 알고 공부하는 것이 중요하다고 생각한다. 이유를 가지고 좀 더 흥미롭게 임해야 집중도를 높일 수 있다고 생각하기 때문이다.
타입스크립트는 자바스크립트에 타입을 부여한 언어이다.
타입스크립트는 자바스크립트와 달리 브라우저에서 실행하려면 파일을 한 번 변환 해주어야 합니다. 이 변환 과정을 우리는 컴파일(complile) 이라고 부른다.
TypeScript는 언어이자 컴파일러(tsc)이다. 컴파일러는 TypeScript를 JavaScrite로 바꿔준다.
// JS
function sum(a, b) {
return a + b;
}
sum (10,20); // 30
sum ("10", "20") // "1020"
// TS
function sum(a: number, b: number) {
return a + b;
}
sum(10,20) // 30
sum('10', '20'); // Error: '10'은 number에 할당될 수 없습니다.
이와 같이 자바스크립트는 Dynamic Typing으로 자유도와 유연성으로 찾아오는 단점을 Static Typing인 타입스크립트가 해결을 할 수 있다. (아래 사진과 같이 에러를 친절하게 알려준다는 장점도 있다.)
type
interface
as
let a = 123;
a = "hi" as unknown as number
// 억지로 타입을 변경해 주었다.
// a는 string일수 없지만 number타입으로 변경해주어 에러가 발생하지 않는다.
// 위 TypeScript의 JavaScript코드는 이와 같이 바뀐다.
let a = 123;
a = "hi"
body없는 function
// 위의 코드를 이런식으로 작성도 가능하다.
function sum(a: number, b: any):number;
function sum(a,b){
return a + b;
}
// 위 코드는 JavaScript에서는 아래와 같이 변환된다.
function sum(a,b){
return a + b;
}
enum
리터럴
declare
TypeScript에서 좁은 타입은 더욱 구체적인 타입이 좁은 타입이라고 말할 수 있다
type A = string | number
type B = number
type C = string & number // C= never
tpye A는 string 또는 number로 두 가지의 타입이 추론될 수 있지만, type B는 number 한 가지의 타입이 추론될 수 있다. C라는 타입은 never로 출력되고 존재하지 않는 타입을 추론한다. 왜냐하면 string이면서 number일 수 있는 타입은 없기 때문이다. 타입이 큰 순서대로 나열하자면 A > B > C 순서일 것이다. 밴다이어그램의 비슷한 윈리로 any는 전체집합으로 never는 공집합으로 볼 수 있다.
위 설명과 코드에 대해 생각해본다면 좁은 타입에서 넓은 타입으로만 대입이 가능하다는 것을 알 수 있을 것이다.
즉, type B는 type A에 대입이 가능하다는 것을 알 수 있다. 하지만 객체 형태일 경우에는 다르니 주의해야한다.
type A = {name : string};
type B = {age : number};
type AB = A|B;
type C = {name: string, age: number}; // C = A & B로 표현가능
위와 같은 경우에 좁은 타입은 type C가 제일 좁은 타입이다. 객체의 경우에는 더 상세하고 구체적일수록 좁은 타입인 것이다. 그렇다면 가장 넓은 타입은 type AB가 될 수 있음을 알 수 있다.
좁은 타입에서 넓은 타입으로만 대입이 가능하기 때문에 아래와 같이 대입해볼 수 있다.
const c: C = {name: 'kim', age: 26}
const ab: AB = c
하지만 넓은 타입에서 좁은 타입으로 대입은 불가능하다.
const ab: AB = {name: 'kim'}
const c: C = ab // error:' age' 속성이 'A' 형식에 없지만 'C' 형식에서 필수입니다
type A = {name : string};
type B = {age : number};
type AB = A|B;
type C = A & B; // {name: string, age: number}
const ab: AB = {name: 'kim'}
const c: C ={name: 'kim', age: 26, married:false} // error
JavaScript라면 C라는 객체에 속성을 추가하거나 따로 에러를 띄우지 않겠지만 TypeScript의 경우 잉여 속성 검사를 통해 오타나 의도치 않은 속성을 입력했을때 등의 에러를 방지해준다.
하지만 아래와 같이 에러를 해결하는 우회 방법이 있다.
const obj = {name: 'kim', age: 26, married:false}
const c: C = obj
function padLeft(padding: number | string, input: string) {
return " ".repeat(padding) + input;
}
repeat()메서드는 문자열을 주어진 횟수만큼 반복해 붙인 새로운 문자열을 반환합니다. 하지만 padLeft함수의 padding매개변수는 타입이 number 또는 string이기 때문에 아래와 같은 에러가 발생한다.
TypeScript에서 이와 같은 에러를 해결하기 위해서는 명시적이고 정확하게 작성해 타입을 좁혀주어야한다.
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
또한 아래와 같이 Class자체를 타입으로 지정할 수 있다.
class Aclass {
aaa() {}
}
class Bclass {
bbb() {}
}
function aORb(ab: Aclass | Bclass){
ab.aaa();
}
함수 aORb의 매개변수를 클래스 자체의 타입으로 지정할 수 있는데 이는 aORb(new(A) | new(B)) 라는 인스터스를 의미한다. 하지만 위 코드는 에러를 발생한다.
위 에러를 아래와 같이 해결할 수 있다.
class Aclass {
aaa() {}
}
class Bclass {
bbb() {}
}
function aORb(ab: Aclass | Bclass){
if(ab instanceof Aclass){
ab.aaa();
}
}
instanceof연산자는 생성자의prototype속성이 객체의 프로토타입 체인 어딘가 존재하는지 판별합니다.
type을 이용하는 경우 속성의 값을 이용해서 구분이 가능하다.
type X = {type: 'x', xxx: string};
type Y = {type: 'y', yyy: string};
type Z = {type: 'z', zzz: string};
function typeCheck(a: X | Y | Z){
if(a.type === 'x'){
a.xxx;
} else if (a.type === 'y'){
a.yyy;
} else {
a.zzz
}
}
또한 in 연산자를 이용하여 속성 자체로 구분이 가능하다.
type X = {type: 'x', xxx: string};
type Y = {type: 'y', yyy: string};
type Z = {type: 'z', zzz: string};
function typeCheck(a: X | Y | Z){
if('xxx' in a){
a.xxx;
} else if ('yyy' in a){
a.yyy;
} else {
a.zzz
}
}
interface Cat { meow: number }
interface Dog { bow: number }
function catOrDog(a: Cat | Dog): a is Dog {
// is를 사용하여 타입 판별을 커스텀 할 수 있다.
if ((a as Cat).meow) { return false }
return true;
}
catOrDog함수의 return 값이 true라면, type predicate 그대로 '함수가 호출된 범위 내에선 a를 string타입으로 보라' 는 것이다.
간단한 실전 예시로 is연산자의 타입가드를 한번 더 확인해 보자.
const isRejected = (input: PromiseSettledResult<unknown>): input is PromiseRejectedResult => {
return input.status === 'rejected'
}
const isFulfilled = <T>(input: PromiseSettledResult<T>): input is PromiseFulfilledResult<T> => {
return input.status === 'fulfilled'
};
const promises = await Promise.allSettled([Promise.resolve('a'), Promise.resolve('b')]);
const errors = promises.filter(isRejected); // 에러
const sucess = promises.filter(isFulfilled); // 성공
export {}
promise를 실행하게 되면 비동기 처리가 아직 수행되지 않은 pending상태가 된다. 그 이후 비동기 처리가 수행된 상태가 (성공 또는 실패) 되면settled상태과 되며 성공과 실패 여부에 따라 최종적으로 fulfilled, rejected 상태를 가진다.
Promise로 구현된 비동기 함수는 Promise 객체를 반환하여야 한다. Promise로 구현된 비동기 함수를 호출하는 측(promise consumer)에서는 Promise 객체의 후속 처리 메소드(then, catch)를 통해 비동기 처리 결과 또는 에러 메시지를 전달받아 처리한다.
then은 fulfilled를 catch는 rejected 상태를 의미한다.
즉, 위 코드에서 PromiseSettledResult는 PromiseRejectedResult 또는 PromiseFulfilledResult를 의미함을 알 수 있다.

함수 isRejected에서 input is PromiseRejectedResult로 타입가드를 선언하여 return 값이 true일 경우 isRejected함수의 반환값의 타입을 PromiseRejectedResult로 보아라 라는 의미인 것이다. isFulfilled 함수도 동일한 방식이다.
errors 나 success를 직접적으로 아래와 같이 표현하여도 될 것 같지만 TypeScript는 타입을 넓게 추론하여 올바른 추론이 되지않아 is연산자를 통해 타입을 정확하고 좁게 타입을 선언하는 것이다.
const isRejected = (input: PromiseSettledResult<unknown>): input is PromiseRejectedResult => {
return input.status === 'rejected'
}
const isFulfilled = <T>(input: PromiseSettledResult<T>): input is PromiseFulfilledResult<T> => {
return input.status === 'fulfilled'
};
const promises = await Promise.allSettled([Promise.resolve('a'), Promise.resolve('b')]);
const errors = promises.filter((a) => a.status === "rejected");
const success = promises.filter((a) => a.status === "fulfilled");
export {}
우리는 errors가 PromiseRejectedResult라는 타입을 가지기를 바라지만 TypeScript는 PromiseSettledResult로 넓게 타입을 추론하는 이런 문제를 is연산자를 사용한 isRejected를 이용하여 해결할 수 있는 것이다.
사용하고 있는 IDE에서 TypeScript을 사용할 때 tsc --noEmit을 알아서 실행시켜 발생한 에러를 보여주면서 타입추론을 해준다.
const a: string = "5"
위 코드는 TypeScript에서 올바른 코드일까?
사실 이것은 올바른 TypeScript의 코드가 아니다. 왜냐하면 const로 선언한 a는 상수로써 "5"의 값을 가지기 때문에 값은 변하지 않는다.
const a = "5"
이와 같이 코드를 작성하면 TypeScript는 아래와 같이 타입을 "5"라고 잘 정의해주는 것을 볼 수 있다. 
첫 번째 코드에서는 타입을 string으로 선언해 줌으로써 타입이 바뀌게 된 것이다.
정확한 타입을 위해서는 이러한 점을 주의해야 하지만 회사와 각 조직마다 다르게 생각하는 것 같아 정확하게 맞다 틀리다 따지지 못하겠다.
확실한 것은 타입추론을 위해서는 TypeScript가 타입추론을 잘못했거나 any라는 타입으로 타입을 추론하고 있다면 변경해야하는 것이 맞다.
function sum(a: number, b:number) {
return a + b;
}
const result = sum (1,2)
위 코드에서 TypeScript는result을 number라는 타입으로 추론하게 된다.
function sum(a: number, b: any){
return a + b;
}
const result = sum (1,2)
하지만 이와 같이 매게변수 b가 any라는 타입을 가지게 되면 result 또한 any 타입을 가지게 된다. result를 number타입으로 추론되게 하고 싶다면 아래와 같이 함수 자체의return 값의 타입을 지정하여 타입을 좁게 설정해주면 된다. (any를 무분별하게 사용하는 것이 올바르지 않다)
function sum(a: number, b: any):number {
return a + b;
}
const result = sum (1,2) // result의 타입은 number가 된다.
아래의 코드에서는 head라는 상수의 타입을 Element 또는 null 이라는 타입으로 추론하고 있다.
하지만 null이나 undefiend가 아님을 증명한다는 의미로 아래와 같이 작성할 수 있다.
이렇게 !를 추가하여 null과 undefiend를 제외한 타입을 추론 할 수 있다.
하지만 이러한 방식은 개발자의 실수가 있을 수 있고 불안하기 때문에 아래와 같이 작성하는 것이 안정적이다.
const head = document.querySelector('#head');
if(head) {
head.innerHTML = "hello" ;
}
JavaScript의 템플릿 리터럴 문자열과 구문이 동일 합니다.
구체적인 리터럴 유형과 함께 사용하는 경우 템플릿 리터럴은 내용을 연결하여 새로운 문자열 리터럴 유형을 생성합니다.

전개 구문을 사용하여 배열에서 제공되는 나머지 인수의 개수를 제공할 수 있다.

keyof 연산자는 객체 타입에서 객체의 키 값들을 숫자나 문자열 리터럴 유니언을 생성합니다. 아래 타입 P는 “x” | “y”와 동일한 타입입니다.
type Point = { x: number; y: number };
type P = keyof Point; // Point의 key값인 "x" 또는 "y"를 타입으로 가집니다.
const a: P = "x"
const b: P = "Y"
JavaScript에서는 이미 표현식 컨텍스트에서 사용할 수 있는 typeof 연산자가 있습니다.
console.log(typeof "Hello world"); // "string"
TypeScript는 타입 컨텍스트에서 변수나 프로퍼티의 타입을 추론할 수 있는 typeof 연산자를 추가합니다.
let s = "hello";
let n: typeof s; // n의 타입추론 결과는 let n: string
keyof와 typeof를 같이 사용
const obj = {a:"123", b:"hello", c:"world"} as const
type Key = typeof obj
위 코드에서 Key라는 타입은 아래와 같이 추론된다.
이제 keyof를 사용해서 obj의 key값을 타입으로 지정해줄 수 있다.
const obj = {a:"123", b:"hello", c:"world"} as const
type Key = keyof typeof obj
Key라는 타입은 이제 obj의 key값을 타입으로 가지며 타입을 확인해보면 아래와 같이 지정된 것을 알 수 있다.
만약 value값을 타입으로 지정하고 싶다면 아래와 같이 작성하여 지정할 수 있다.
const obj = {a:"123", b:"hello", c:"world"} as const
type Key = typeof obj [keyof typeof obj] // typeof obj [a] 형식인 것이다.
Key라는 타입은 이제 obj의 value값을 타입으로 가지게 된다.
주의할 점은 as const가 없다면 Key라는 타입은 string을 타입으로 가지게 된다는 것이다.
enum은 이름이 있는 상수들의 집합을 정의할 수 있습니다. enum을 사용하면 구분되는 집합을 더 쉽게 만들수 있습니다.
Up은 0이라는 값을 가지고 그 지점부터 뒤따르는 멤버들은 자동으로-증가된 값을 갖습니다. 즉 EDirection.Up 은 0, Down은 1, Left는 2, Right은 3을 값으로 가집니다.
const enum EDirection {
Up,
Down,
Left,
Right,
}
// enum을 아래와 같이 작성하여 자바스크립트 파일에도 남기도록 할 수 있다.
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
// Using the enum as a parameter
function walk(dir: EDirection) {
conosole.log(dir)
}
// It requires an extra line to pull out the keys
type Direction = typeof ODirection[keyof typeof ODirection];
// [설명] typeof ODirection [Up] 형식이다. Up의 value값(0)을 타입으로 지정된다.
function run(dir: Direction) {
console.log(dir)
}
walk(EDirection.Left); // 2
run(ODirection.Right); // 3
[Typescript handbook Link]
타입 별칭은 특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미합니다.
똑같은 타입을 한 번 이상 재사용하거나 또 다른 이름으로 부르고 싶은 경우 사용됩니다.
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 });
interface로 표기
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 BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
}
위와 같이 AddressWithUnit인터페이스는 unit속성을 제외한 BasicAddress인터페이스의 속성을 모두 포함하고 있을 경우 아래와 같이 인터페이스끼리의 상속이 가능하다.
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
// AddressWithUnit는 BasicAddress을 상속받아 확장된다.
interface AddressWithUnit extends BasicAddress {
unit: string;
}
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
// ColorfulCircle 인터페이스는 Colorful과 Circle을 상속받아 확장된다.
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
[Typescript handbook Link]
implements를 사용하여 특정 클래스가 interface에 총족하는지 확인할 수 있다. 클래스를 올바르게 구현하지 못했다면 오류를 발생시킨다.
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable { // error
pong() {
console.log("pong!");
}
}

extends와 implements의 차이점
extends키워드는 새로운 클래스의 '상속'을 위해 사용한다. 상위 클래스의 모든 프로퍼티와 메서드들을 갖고 있으므로 일일이 정의하지 않아도 된다. 상위 클래스의 프로퍼티를 지정하지 않으면, 초기값으로 선언되며 에러는 반환하지 않는다.
implements키워드는 새로운 클래스의 모양을 동일하게 정의하고 싶을 때 사용한다. 따라서,interface로 정의한 값들은 모두 필수적으로 들어가야 하며, 하나라도 빠질 경우 에러를 반환한다. 타입으로 지정한 메서드 모두 내부에서 재정의가 필요하다.
[Typescript handbook Link]
TypeScript는 기존 객체 유형을 결합하는데 주로 사용되는 Intersection Type 이라는 또 다른 구조를 제공합니다. Intersection Type은 &연산자를 사용하여 정의됩니다.
type Animal = {breath: true}
type Mammal = Animal & {eat: true}
type Human = Mammal & {think: true}
const kim: Human = {breath: true, eat: true, think: true}
interface와 type간 상호작용이 가능하다.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
[Typescript handbook Link]
때로는 TypeScript보다 우리가 어떤 값의 타입에 대한 정보를 더 잘 아는 경우도 존재한다.
예를 들어 코드상에서 document.getElementById가 사용되는 경우, TypeScript는 이때 HTMLElement 중에 무언가가 반환된다는 것만을 알 수 있는 반면에, 당신은 페이지 상에서 사용되는 ID로는 언제나 HTMLCanvasElement가 반환된다는 사실을 이미 알고 있을 수도 있습니다.
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

as HTMLCanvasElement를 통해 타입을 단언해 줌으로써 타입을 좀 더 구체적으로 명시할 수 있습니다.
[Typescript handbook Link]
void 반환 타입으로의 문맥적 타이핑은 함수를 아무것도 반환하지 않도록 강제하지 않는다.void 반환 타입을 가지는 문맥적 함수 타입(type vf = () => void)가 구현되었을 때, 아무값이나 반환될 수 있지만, 무시가 된다.
그러므로 아래와 같이 타입 () => void의 구현들은 모두 에러가 발생하지 않는다.
type voidFunc = () => void;
const f1: voidFunc = () => {
return true;
};
const f2: voidFunc = () => true;
const f3: voidFunc = function () {
return true;
};
const v1 = f1();
const v2 = f2();
const v3 = f3();
// 모두 반환값이 다른 변수에 할당될 때도 여전히 void타입을 유지한다.
단,유의해야 할 한 가지 다른 경우가 있다. 리터럴 함수 정의가 void 반환 값을 가지고 있다면, 그 함수는 어떠한 것도 반환해서는 안된다.
function f2(): void {
return true;
// 'boolean' 형식은 'void' 형식에 할당할 수 없습니다
}
const f3 = function (): void {
return true;
//'boolean' 형식은 'void' 형식에 할당할 수 없습니다
};
즉, 함수를 void로 선언한 것과 매서드로 선언할 때의 void와 매개변수로 선언한 void가 차이점을 가진다.
매서드와 매개변수로 선언한 void는 반환값을 사용하지 않겠다라는 의미이고 함수fmf void로 선언한 경우는 반환값이 없다라는 의미로 해석할 수 있다.

상수 test는 Test인터페이스의 타입으로 추론되었고 result는 test의 talk요소에 접근하였다. test는 3을 반환하도록 되어있지만 Test이 타입이 반환값을 void로 추론하기 때문에 3을 반환하도록 작성할 수는 있지만 무시가되어 result의 타입 추론 또한 vold로 추론이 되는 것이다.
[Typescript handbook Link]
인덱스 시그니처(Index Signature)는 { [Key: T]: U } 형식으로 객체가 여러 Key를 가질 수 있으며, Key와 매핑되는 Value를 가지는 경우 사용합니다.
interface objSalary {
bouns: 200,
pay: 2000,
allowance: 100,
incentive: 100
}
function totalSalary(salary: {[key: string]: number}) {
let total = 0;
for (const key in salary) {
total += salary[key];
}
return total;
}
위와 같은 예시로 객체 내부에 존재하는 속성의 값을 모두 합산해야 하는 경우 인덱스 시그니처를 사용할 수 있습니다.
[Typescript handbook Link]
맵드 타입이란 기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 문법을 의미합니다. 마치 자바스크립트 map() API 함수를 타입에 적용한 것과 같은 효과를 가집니다.
아래와 같이 Apple, Banana, Orange 유니온 타입으로 묶어주는 Fruit라는 타입이 있다고 하겠습니다.
type Fruit = 'Apple' | 'Banana' | 'Orange';
여기서 이 세 과일의 이름에 각각 가격까지 붙인 객체를 만들고 싶다고 한다면 아래와 같이 변환할 수 있습니다.
type FruitPrice = { [key in Heroes]: number };
const fruitInfo: FruitPrice = {
Apple: 500,
Banana: 1000,
Orange: 700,
}
위 코드에서 [key in Heroes] 부분은 마치 자바스크립트의 for in 문법과 유사하게 동작합니다. 앞에서 정의한 Fruit 타입의 3개의 문자열을 각각 순회하여 number 타입을 값으로 가지는 객체의 키로 정의가 됩니다
{ Apple: number } // 첫번째 순회
{ Banana: number } // 두번째 순회
{ Orange: number } // 세번째 순회
따라서 위의 원리가 적용된 FruitPrice의 타입은 아래와 같이 정의됩니다.
type FruitPrice = {
Apple: number;
Banana: number;
Orange: number;
}
참고자료
https://joshua1988.github.io/ts/why-ts.html#%EC%99%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A5%BC-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C%EC%9A%94
https://yamoo9.gitbook.io/typescript/ts-vs-es6/arrow-function
https://www.inflearn.com/course/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%98%AC%EC%9D%B8%EC%9B%90-1/dashboard
https://developer-talk.tistory.com/297