const str: string = 'hello';
const num: number = 123;
const bool: boolean = false;
const n: null = null;
const u: undefined = undefined;
const sym: symbol = Symbol('sym');
const big: bigint = 100000000n;
const obj: object = {hello: 'world'};
function plus(x: number, y: number): number {
return x + y;
}
const minus = (x: number, y: number): number => x - y;
타입스크립트는 어느 정도 변수와 반환값의 타입을 스스로 추룐할 수 있다.
다만, 매개변수에는 타입을 부여해야 한다.
"타입스크립트가 타입을 제대로 추론하면 그대로 쓰고, 틀리게 추론할 때만 올바른 타입을 표기한다."
const str = "hello"; // str: "hello"
const num = 123; // num: 123
const bool = false; // bool: false
const n = null; // n: null
const u = undefined; // u: undefined
const sym = Symbol('sym'); // sym: typeof sym
const big = 100000000n; // big: 100000000n
const obj = {hello: 'world'}; // obj: {hello: string}
직접 타입을 표기한다면 넓은 타입을 표기할 여지가 있다.
다음 코드는 아무런 에러가 없다.
const str1: "hello" = "hello";
const str2: string = "hello";
const str3: {} = "hello";
참고로 {} 타입은 객체를 의미하는 것이 아니라 null과 undefined를 제외한 모든 타입을 의미한다.
이러한 이유로 타입스크립트가 제대로 추론했다면 그대로 사용하고 잘못 추론했다면 그때 직접 타입을 표기한다.
let str = "hello"; // str: string
let num = 123; // num: number
let bool = false; // bool: boolean
let n = null; // n: any
let u = undefined; // u: any
let sym = Symbol('sym'); // sym: symbol
let big = 100000000n; // big: bigint
let obj = {hello: 'world'}; // obj: {hello: string}
특이한 점
null과 undefined를 대입하면 any 타입으로 추론한다.sym은 const일 때는 typeof sym 이지만, let일 때는 symbol이다. typeof sym은 고유한 symbol(=unique symbol)을 의미하고 symbol은 일반적인 symbol을 의미한다.Note 타입스크립트의 에러를 무시하려면
// @ts-ignore
if(sym1 === sym2){}
에러가 나는 코드 윗줄에 @ts-ignore주석을 달면 된다.
@ts-expect-error주석도 있다.
@ts-ignore : 다음 줄의 코드가 원래 에러가 있든 없든간에 무시한다.@ts-expect-error : 다음 줄의 코드가 에러가 나야하는데 안나면 에러를 발생한다. 그래서 예측할 수 있는 @ts-expect-error를 권장한다.
원시 자료형에 대한 리터럴 타입 외에도 객체를 표시하는 리터럴 타입이 있다
const obj: { name: "kdh" } = { name: "kdh" };
const arr: [1, 2, "three"] = [1, 2, "three"];
const func: (amount: number, unit: string) => string = (amount, unit) =>
amount + unit;
이 경우, 타입스크립트는 대부분의 경우 의도한 것보다 더 부정확하게 추론한다.
const obj = {name: 'kdh'}; // obj: {name: string}
const arr = [1, 2, 'three']; // arr: (string|number)[]
타입스크립트는 수정 가능성을 염두에 두고 타입을 넓게 추론한다.
값이 변하지 않는 것이 확실하다면 as const 접미사를 붙이면 도니다.
const obj = { name: "kdh" } as const; // obj: {readonly name: 'kdh'}
const arr = [1, 2, "three"] as const; // arr: readonly [1, 2, 'three]
as const 이후에 obj.name을 변경하거나, arr에 값을 추가/삭제/수정 한다면 에러가 발생한다.
배열을 표기하는 2가지 방법
const arr1: string[] = ['a', 'b', 'c'];
const arr2: Array<string> = ['a', 'b', 'c'];
배열 내에 여러 타입이 들어갈 때는 다음과 같이 추론한다.
const arr3 = [1, 2, 3]; // arr3: number[]
const arr4 = [1, '2', 3]; // arr4: (string|number)[]
const arr5 = []; // arr5: any[]
이러한 추론에는 한계가 있다.
arr1[100].toFixed(); // arr1[100]이 undefined인데도 에러 발생안함.
각 요소 자리에 타입이 고정되어 있는 배열을 튜플이라고 한다.
const tuple: [number, boolean, string] = [1, false, 'hi'];
tuple[100].toFixed(); // 에러
다만 튜플도 push,pop,unshift,shift 메서드를 통해 요소를 추가/삭제하는 것은 에러를 발생시키지 않는다.
추가하더라도 tuple[100] 처럼 접근하는 순간 에러를 발생시키므로 의미가 없다.
push까지 막으려면 readonly 키워드를 붙이면 된다.
각 요소 자리에 타입이 고정되어 있는 배열일 뿐, 자리 개수가 고정된 배열이 아니다.
const strNumBools: [string, number, ...boolean[]] = ['hi', 123, true, false, true];
spread문법으로 얼마든지 길이를 조정할 수 있다.
물론 타입이 아니라 값에 spread 문법을 적용해도 알아서 추론한다.
const arr1 = ['hi', true]; // arr1: (string|boolean)[]
const arr2 = [46, ...arr1]; // arr2: (string|number|boolean)[]
const [arr3, ...arr4] = ['hi', 1, 2, 3];
// arr3: string, arr4: [number, nuber, number]
const [arr5, ...arr6]: [string, ...number[]] = ['hi', 1, 2, 3];
// arr5: string, arr6: number[]
타입 뒤에 ? 가 붙을 수 있다. 이는 옵셔널 수식어로 해당 자리에 값이 있어도 그만, 없어도 그만 이라는 뜻이다.
[number, boolean?, string?] 의 가능한 표현
[3][3, true][3, true, 'abc']주의: 순서를 지킬 것
[3, 'abc'] 는 안됨. boolean 위치를 생략하려면 [3, , 'abc'] 로 사용
타입을 값으로 사용할 수는 없다.
하지만 값은 타입으로 사용할 수 있는 것과 없는 것이 있다.
타입으로 사용할 수 있는 값
typeof 변수대신 string, object, number, boolean, symbol를 사용하자.
왜냐하면 Number 타입끼리 연산자를 사용할 수 없고, string 타입에 String 타입을 대입할 수도 없다.
유니언 타입을 사용할 때는 타입 네로잉으로 타입을 좁혀야 한다.
let strOrNum: string | number = 123;
if(typeof strOrNum === 'number'){
strOrNum.toFixed(); // strOrNum: number
}
여기서 typeof 연산자는 자바스크립트의 연산자이다. 타입스크립트의 키워드가 아니다.
유니언 타입의 공통으로 포함된 속성이 있다면 타입 네로잉을 하지않아도 된다.
참고로 자바스크립트에서는 parseInt() 의 인수로 숫자와 문자열 둘 다 가능하지만 타입스크립트에서는 문자열만 가능하다.
그래서 다음과 같은 표기는 에러를 발생시킨다.
let strOrNum: string | number = 1;
parseInt(strOrNum);
유니언의 문법적 특징이 하나 있다.
타입 사이에만 |연산자를 쓸 수 있는 것이 아니라 타입 앞에도 사용할 수 있다.
type Union1 = | string | boolean | number;
type Union2 =
| string
| boolean
| number
"any 타입은 타입 검사를 포기한다는 선언과 같다."
any 타입을 통해 파생되는 결과물도 any 타입이 되므로 초장부터 타입을 직접 표기해야 한다.
특이한 점
any[]로 추론된 배열에 push 메서드나 인덱스로 요소를 추가하면 추가할 때마다 추론하는 타입이 바뀐다
const arr = []; // arr: any[]
arr.push('a'); // arr: string[]
arr.push(1); // arr: (string | number)[]
arr[2] = true; //arr: (string | number | boolean)[]
다만 pop메서드로 제거를 하더라도 이전 타입으로 되돌아 가지는 않는다.
any 타입은 숫자나 문자열 타입과 연산할 때 타입이 바뀌기도 한다.
const a: any = '123';
const an1 = a + 1; // an1: any
const an2 = a - 1; // an2: number
const an3 = a * 1; // an3: number
const an4 = a / 1; // an4: number
const st = a + '1'; // st: string
타입스크립트가 명시적으로 any를 반환하는 경우
JSON.parse와 fetch함수
fetch("url")
.then((response) => {
return response.json();
})
.then((result) => {}); // result: any
const result = JSON.parse('{"hello":"json"}'); // result: any
이때는 직접 타이핑 해야한다.
fetch("url")
.then<{data: string}>((response) => {
return response.json();
})
.then((result) => {}); // result: {data: string}
const result: {hello: string} = JSON.parse('{"hello":"json"}');
// result: any
unknown은 any처럼 모든 타입을 대입할 수 있지만, 그 후 어떠한 동작도 수행할 수 없음.
any는 타입 검사를 포기 한 채로 모든 타입이 대입이 되지만
unknown은 타입 검사 안에서 모든 타입이 대입되는 타입이다.
대부분의 경우 try catch문에서 unknown을 보게 된다.
try{
}catch(e){ // e: unknown
console.log(e.message); // 에러. unknown타입은 속성에 접근할 수 없다.
}
이 경우 다음처럼 사용한다.
try{
}catch(e){
const error = e as Error;
console.log(err.message);
}
as 타입 은 Type Assertion이라 한다.
강제로 타입을 주장하는 것이다.
항상 Type Assertion이 먹히는 건 아니다.
string에서 number로 변경할 수 없다.
이 때는 unknown을 경유해서 변경하면 된다.
const a: number = '123' as number; // 에러
const b: number = '123' as unknown as number // 통과
Not Null Assertion 연산자(!)
이름과 달리 null 뿐만 아니라 undefined도 아님을 주장하는 연산자이다.
function a(param: string | null | undefined){
param.slice(3); // 에러 param이 null이나 undefined일 수 있기때문
param!.slice(3); // 통과
}
함수의 반환값이 없는 경우 void로 추론된다.
참고로 자바스크립트에서는 반환값이 없는 경우 자동으로 undefined값이 반환된다.
화살표 함수 형태의 타입의 유일한 반환값으로 사용될 때 반환값을 무시한다.
화살표 함수 형태의 타입이 아닌 반환값의 타입을 직접 입력하거나, void 외에 다른 타입을 유니온으로 반환하더라도 반환값을 무시하지 않는다.
const func: () => void = () => 3; // 반환값 무시
const value = func(); // value: void
const func2 = (): void => 3; // 에러
const func3: () => void | undefined = () => 3; // 에러
반환값을 사용하지 않는 콜백 함수를 타이핑하기 위한 조치이다.
정리하면, void는 두 가지 목적을 위해 사용된다.
1. 사용자가 함수의 반환값을 사용하지 못하도록 제한한다.
2. 반환값을 사용하지 않는 콜백 함수를 타이핑할 때 사용한다.
{} 타입은 Object 타입과 같다.
객체로 오해할 수 있겠지만 타입으로 사용될 시 객체가 아닌 null과 undefined을 제외한 모든 타입을 의미한다.
unknown을 보면 모든 타입이 대입될 수 있지만 사용은 불가능하듯이, {} 타입도 null과 undefined가 아닌 타입이 대입될 수는 있지만 사용하는건 별개다.
const obj: {} = {name: 'kdn'};
obj.name; // 에러
직접 사용할 경우는 거의 없다고 본다.
그러나 다음 코드처럼 if문안에서 타입 네로잉을 하면 보게 되므로 알고는 있어야한다.
const unk: unknown = 'hello';
if(unk){
unk; // unk: {}
}
{}, Object 타입과는 다르지만, object 타입도 쓸모없다.
obect타입은 원시값이 아닌 객체를 의미한다.
let a: Object;
a = 123; // ✅ 가능
a = "hello"; // ✅ 가능
a = () => {}; // ✅ 가능
a = null; // ❌ 불가능
let b: object;
b = 123; // ❌ 불가능
b = "hello"; // ❌ 불가능
b = () => {}; // ✅ 가능
b = { x: 1 }; // ✅ 가능
never 타입에는 어떠한 타입도 대입할 수 없다.
never 타입은 존재할 수 없는 값을 의미한다.
예외를 던지는 함수의 리턴값은? 정의할 수 없다.
무한루프를 돌아서 종료하지 않는 함수의 리턴값은? 정의할 수 없다.
이 때 never를 사용한다.
단, 함수 표현식에서만 해당된다.
함수 선언문은 void로 추론한다.
function func1() {
throw "error"; // 반환 타입이 void
}
const func2 = () => {
throw "error"; // 반환 타입이 never
};
function func3() {
while(true){}; // 반환 타입이 void
}
const func4 = () => {
while(true){}; // 반환 타입이 never
};
그래서 함수 선언문에서는 never로 타이핑해줘야 한다.
타입 네로잉으로 타입을 거르면 맨 마지막에 남는 타입도 never이다.
function func(param: string|number){
if(typeof param === 'string'){}
else if(typeof parma === 'number'){}
else{
param; // never타입, 사실상 실행되지 않는 코드 구간임.
}
}
| ⬅️ | any | unknown | Object | void | undefined | null | never |
|---|---|---|---|---|---|---|---|
| any | O | O | O | O | O | O | O |
| unknown | O | O | O | O | O | O | O |
| Object | O | X | O | X | X | X | O |
| void | O | X | X | O | O | X | O |
| undefined | O | X | X | X | O | X | O |
| null | O | X | X | X | X | O | O |
| never | X | X | X | X | X | X | O |
type A = string;
const str: A = 'hello';
타입 별칭은 type 키워드를 사용해서 선언한다.
타입 별칭의 이름 규칙은 파스칼 케이스가 국룰이다.
복잡한 타입이거나 가독성을 높이기 위해 타입 별칭을 사용한다.
const person1: {
name: string;
age: number;
} = {
name: "kdh",
age: 23,
}; // 가독성 낮고 복잡하다.
type Person = { // 타입을 따로 분리
name: string;
age: number;
};
const person2: Person = {
name: "kdh",
age: 30,
}; // 가독성 굿
타입 별칭으로 타입을 분리하면 재사용할 수 있어 좋다.
객체 타입에 이름을 붙이는 또 하나의 방법은 인터페이스이다.
interface Person{
name: string,
age: number;
married: boolean
}
속성을 구분할 때 콤마, 콜론, 줄바꿈으로 구분할 수 있지만 하나로 통일하자.
인터페이스로 배열과 함수도 타이핑할 수 있다.
interface Func{
(x:number, y:number): number;
}
const add: Func = (x, y) => x + y;
interface Arr{
length: number;
[key: number]: string; // 인덱스 시그니처
}
const arr: Arr = ['a', 'b', 'c'];
속성의 키 자리에 [key: number] 라는 문법이 있는데 인덱스 시그니처 라고 부른다.
위의 Arr타입은 정확히 말하면 배열타입은 아니다.
배열이 제공하는 메서드들이 정의되지 않았기 때문이다.
참고로 자바스크립트의 객체의 속성 키는 문자열과 심볼만 가능하다.
속성에 숫자를 넣어도 자바스크립트가 자동으로 문자열로 만들고 저장한다.
타입스크립트에서는 배열 타이핑의 편의성을 위해 number로 하는 것을 허용한다.
특별한 점
빈 인터페이스는 {} 타입과 유사하게 null과 undefined를 제외한 값들을 대입할 수 있다.
인터페이스끼리는 서로 합쳐진다.
같은 이름으로 여러 인터페이스를 선언할 수 있다. 이러면 인터페이스가 하나로 합쳐진다.
이를 선언 병합(declaration merging)이라고 부른다.
다만 속성이 겹치는 데 타입이 다르다면 에러가 발생한다. 타입이 완전히 같아야 한다.
의도치 않게 병합되는 걸 막기위한 조치들 중 하나로 네임스페이스가 있다.
namespace Example{
export interface Inner{
test: string;
}
export type test2 = number;
}
const ex1: Example.Inner = {
test: "hello"
};
const ex2: Example.test2 = 123;
export 키워드를 이용해 namespace 안의 속성들을 외부에도 노출시켜야 한다.
namespace를 중첩할 수도 있다. 이 경우 안쪽의 namespace도 export 해야한다.
네임스페이스는 타입스크립트에서 제공하는 문법이지만 namespace 안에 값을 선언할 수도 있다.
namespace Example {
export interface Inner {
test: string;
}
export type test2 = number;
export const a = "real1"; // 진짜 값
export const b = "real2"; // 진짜 값
}
const ex1: Example.Inner = {
test: "hello",
};
const ex2: Example.test2 = 123;
const c = Example.a; // 네임스페이스 내부의 값 접근 (. 연산자)
const d = Example['b']; // 네임스페이스 내부의 값 접근 ([] 연산자)
네임스페이스 내부의 값에 접근할때는 . 말고도 [] 연산자로 접근가능하지만 일반 타입을 접근할 때는 . 연산자만 접근할 수 있다.
위의 코드를 자바스크립트로 변환하면 아래와 같다.
"use strict";
var Example;
(function (Example) {
Example.a = "real1";
Example.b = "real2";
})(Example || (Example = {}));
const ex1 = {
test: "hello",
};
const ex2 = 123;
const c = Example.a;
물론 namespace에 타입만 정의되어 있다면 자바스크립트 변환시 namespace 부분은 통째로 사라진다.
인터페이스의 의도치 않는 병합을 피하기 위해 네임스페이스를 사용하지만, 네임스페이스마저 이름이 똑같다면 병합된다.
완전히 피하기 위해서는 모듈 파일을 이용한다.
인터페이스로 선언했든 타입 별칭으로 선언했든 상관없이 객체의 속성에 공통적으로 적용되는 특징을 알아보자.
옵셔널과 readonly 수식어 사용가능
interface Example{
a: string;
b?: number;
readonly c: boolean;
readonly d?: symbol;
}
interface Example{
a: string;
}
const example1: Example = { // 객체 리터럴 대입
a: "hello",
b: "error" // 없는 속성
}; // 에러
const obj = {
a: "hello",
b: "error"
}
const example2: Example = obj; // 객체 변수 대입, 통과
객체 리터럴을 대입할때는 잉여 속성 검사(Excess Property Checking)이 실행된다.
객체 변수를 대입할 때는 객체간 대입 가능성을 비교한다.
객체에서도 전개 문법과 나머지 속성을 사용할 수 있다.
const {
prop: { nested, ...rest }, // rest: {a: number, b: boolean}
} = { prop: { nested: "hi", a: 1, b: true } };
구조분해 할당 할때 변수이름을 바꿔서 저장하는 문법이 있다. 이것과 헷갈리지 말자.
const {
prop: { nested: string }, // nested 키의 값을 string이라는 변수에 저장
} = { prop: { nested: "hi" } };
console.log(string);
특정 객체 타입의 속성을 가져오려면 다음과 같이 자바스크립트 객체의 속성에 접근하듯 한다.
type Animal = {
name: string;
}
type N1 = Animal['name']; // N1: string
이전에 namespace의 내부 타입에 접근하려면 . 연산자로만 접근했어야 했는데 이건 반대로 []연산자로만 접근할 수 있다.
이러한 방식을 인덱스 접근 타입이라고 부른다.
속성 자체를 타입으로 가져올 수 있다. 보통 값의 타입을 알아내기 위해 사용된다.
const obj = {
hello: "world",
name: "kdh",
age: 28,
};
type Keys = keyof typeof obj; // Keys: 'hello' | 'name' | 'age'
type Values = typeof obj[Keys]; // Values: string | number
여기서의 typeof 키워드는 자바스크립트 연산자가 아니라 타입스크립트의 키워드이다.
typeof 를 사용하는 예제const greeting = "안녕하세요";
let anotherGreeting: typeof greeting = "반갑습니다";
type G = typeof "반갑습니다"; // 에러
// string 타입 자동 추론
const user = {
name: "Kim",
age: 28,
isAdmin: false
};
type UserType = typeof user;
const newUser: UserType = {
name: "Lee",
age: 35,
isAdmin: true
};
function multiply(a: number, b: number): number {
return a * b;
}
type MultiplyType = typeof multiply;
// MultiplyType은 (a: number, b: number) => number
const fruits = {
apple: "red",
banana: "yellow",
grape: "purple"
} as const;
type FruitKeys = keyof typeof fruits;
// "apple" | "banana" | "grape"
type FruitColors = typeof fruits[keyof typeof fruits];
// "red" | "yellow" | "purple"
const config = {
mode: "dark",
version: 3
};
type ConfigType = typeof config;
type ModeType = ConfigType["mode"]; // "dark"
type VersionType = ConfigType["version"]; // number
속성의 이름은 변하지 않기때문에 리터럴 타입으로 가져오지만, 값은 변경가능하기때문에 기본타입으로 가져온다.
객체의 키는 자바스크립트에서 string과 symbol만 허용되지만 타입스크립트에서는 number도 허용한다고 했다.
type Keys = keyof any; // Keys: string|number|symbol
만약 배열 값에 keyof 키워드를 사용하면 어떻게 될까
"number | (배열 속성 이름 유니언) | (배열 인덱스 문자열 유니언)" 이 된다.
type ArrayKeys = keyof [1, 2, 3];
let a: ArrayKeys = 'lastIndexOf';
a = 'length';
a = '0';
a = '1';
a = '2';
a = '3'; // 에러
'3' 은 number 타입이 아니고 배열 속성 이름도 아니고 배열 인덱스 문자열도 아니다.
그치만 a = 3 은 에러가 나지 않는다. 3은 number 타입이기 때문이다.
튜플과 배열에도 인덱스 접근 타입을 사용할 수 있다.
type Arr = [1, 2, 3]; // 리터럴 튜플 타입
type First = Arr[0];
type Length = Arr['length'];
type Values = Arr[number]; // type Values = [1, 2, 3]
interface Example{
a(): void;
b: () => void;
c: {
(): void;
}
}
셋은 거의 같지만 한 가지 경우 다르다. 이 부분은 19절에서 따로 다룬다.
인덱스 시그니처는 객체의 속성 값을 전부 특정 타입(number, string, symbol)으로 만들 수 있었다. 하지만 더 좁은 범위로 제한하고 싶으면 매핑된 객체 타입(Mapped Object Type)을 이용하자.
인터페이스에서는 사용하지 못하고 타입 별칭에서만 가능하다.
type HelloHi = {
[key in 'hello' | 'hi'] = string;
};
in 연산자를 사용해서 시그니처가 표현하지 못하는 타입을 표현한다.
in 연산자 우측에는 유니언 타입이 와야한다.
보통 위와 같이 간단하게는 사용하지않고, keyof 나 typeof를 이용해 유니언 타입을 만들어서 사용한다.
interface Original{
name: string;
age: number;
married: boolean;
}
type Copy = {
[key in keyof Original]: Original[key];
}
튜플과 배열도 객체이므로 매핑된 객체 타입을 적용할 수 있다.
type Tuple = [1, 2, 3];
type CopyTuple = {
[key in keyof Tuple]: Tuple[key];
} // 튜플 대상
type Arr = number[];
type CopyArr = {
[key in keyof Arr]: Arr[key];
} // 배열 대상
const obj = [1, 2, 3];
type Obj = typeof obj;
type CopyObj = {
[key in keyof Obj]: Obj[key];
} // 객체 값 대상
매핑된 객체 타입을 적용하면서 readonly, ? 수식어를 추가 및 제거할 수도 있다.
interface Original{
name: string;
age: number;
}
type Copy = { // readonly, ? 추가
readonly [key in keyof Original]?: Original[key];
}
interface Original2{
readonly name?: string;
readonly age?: number;
}
type Copy2 = { // readonly, ? 삭제
-readonly [key in keyof Original]-?: Original[key];
}
문자열 유틸리티 타입을 이용해서 조합할 수도 있다.
// 문자열 유틸리티 타입
// Uppercase<T> 전체를 대문자로
// Lowercase<T> 전체를 소문자로
// Capitalize<T> 첫 글자만 대문자로
// Uncapitalize<T> 첫 글자만 소문자로
// 예: type Field = "name" | "email";
// type CapitalizedField = Capitalize<Field>; // "Name" | "Email"
type Copy = {
[key in keyof Original as Capitalize<key>]: Original[key];
}