런타임 이전의 컴파일 단계(코드를 입력하는 동안)에서 대부분의 에러를 확인할 수 있다.
런타임은 실제 고객이 마주할 수 있는 환경으로, 고객이 사이트를 이용하는 중 에러가 발생하는 단계라서 에러 발생 시 처리 비용이 크다.
타입을 강제하는 것은 예측 가능한 코드와 디버깅이 쉽게 만든다.
강력한 자동 완성 기능을 통해 생산성을 높인다.
인터페이스, 제네릭 등 객체 지향적 기능을 제공하여 대규모 프로젝트에 적합하다.
타입스크립트의 기본 원리는 구조적 타입 시스템(Structural type system, 일명 '덕 타이핑')
예: dictionary 또는 map이라고 불리는 구조는 안에 key, value가 몇 쌍이 있던 똑같은 모양으로 본다.
특정 타입이 아니어도 특정 타입의 '모양'을 갖고 있으면 그대로 타입 체크를 통과하는 것
$ tsc sample.ts
# compiled to `sample.js`
tsconfig.json 파일로 컴파일러 옵션을 관리할 수 있다.
{
"compilerOptions": {
"strict": true,
"target": "ES6",
"lib": ["ES2015", "DOM"],
"module": "CommonJS"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
기본 타입 외에 다음 타입들이 있다.
정해진 타입으로 이루어진 고정된 길이(length)의 배열을 표현하는 타입
자리도 정해지고 갯수도 정해진다.
let tuple: [string, number];
tuple = ['a', 1];
tuple = ['a', 1, 2]; // Error - TS2322
tuple = [1, 'a']; // Error - TS2322
// 개별 변수
let userId: number = 1234;
let userName: string = 'HEROPY';
let isValid: boolean = true;
// 단일 튜플로 모아서 선언
let user: [number, string, boolean] = [1234, 'HEROPY', true];
console.log(user[0]); // 1234
console.log(user[1]); // 'HEROPY'
console.log(user[2]); // true
let users: [number, string, boolean][]; // 2차원 배열로 튜플 선언
users = [[1, 'Neo', true], [2, 'Evan', false], [3, 'Lewis', true]];
let tuple: [1, number];
tuple = [1, 2];
tuple = [1, 3];
tuple = [2, 3]; // Error - TS2322: Type '2' is not assignable to type '1'.
.push()
나 .splice()
등으로 더 많은 값을 추가할 수 있다.let tuple: [string, number];
tuple = ['a', 1];
tuple = ['b', 2];
tuple.push(3);
console.log(tuple); // ['b', 2, 3] (가능)
tuple.push(true); // (불가능) Error - TS2345: Argument of type 'true' is not assignable to parameter of type 'string | number'.
let a: readonly [string, number] = ['Hello', 123];
a[0] = 'World'; // Error - TS2540: Cannot assign to '0' because it is a read-only property.
enum Week {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat
}
console.log(Week.Sun); // 0
console.log(Week['Sun']); // 0
console.log(Week[0]); // 'Sun'
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue'
}
console.log(Color.Red); // red
console.log(Color['Green']); // green
"noImplicitAny": true
로 설정하면 any를 사용할 때 에러가 발생한다.JSON.parse
메서드(리턴 타입을 추론할 수 없어서 any 사용)let a: any = 123;
let u: unknown = 123;
let v1: boolean = a; // any 타입은 어디든 할당 가능
let v2: number = u; // Error: unknown 값은 any 값을 제외한 다른 곳에는 할당 불가
let v3: any = u; // OK
let v4: number = u as number; // OK: 타입 단언을 사용하면 다른 타입에도 unknown 값을 할당 가능
type Result = {
success: true,
value: unknown
// Union으로 타입 합성 (type aliases)
} | {
success: false,
error: Error // 타입스크립트에서 new Error의 타입은 Error
}
export default function getItems(user: IUser): Result {
if (id.isValid) {
return {
success: true,
value: ['Apple', 'Banana'] // unknown 자리에 배열 할당
};
} else {
return {
success: false,
error: new Error('Invalid user.')
}
}
}
strictNullChecks: true
를 설정하면 null을 포함하지 않는다)let obj: object = {};
let arr: object = [];
let func: object = function () {}; // 함수 포함
let nullValue: object = null; // null 포함
let date: object = new Date(); // 날짜 객체 포함
타입스크립트는 배열을 참조할 때 컴파일 단계에서 에러를 모를 수 있다.
const l = [1, 2, 3]
const item = l[3]
item + 1 // error이지만 컴파일 때는 모름
const user: { [key: string]: string } = { name: 'kyc' }
user.age + 1 // 마찬가지로 error이지만 컴파일 때 모른다
이를 체크하는 방법은 컴파일러 옵션의 noUncheckedIndexedAccess
(But 완벽하지 않음)
const l = [1, 2, 3]
const item1 = l[3]
const item2 = l[2]
item1 + 1 // Object is possibly 'undefined'.(2532)
item2 + 1 // 근데 문제는 이거까지 에러가 난다는 거다
l.map((item) => item + 1) // map으로 돌려서 하는 연산에서는 괜찮음
noUncheckedIndexedAccess
는 이처럼 적당히 경고를 날려주는 장점도 있지만 그다지 똑똑하지 않다는 점도 있다.
결론적으로 객체나 리스트의 값을 인덱싱(참조)할 때는 undefined가 있을 가능성에 대해 염두해 두어야 한다.
let num: number = undefined;
let str: string = null;
let obj: { a: 1, b: false } = undefined;
let arr: any[] = null;
let und: undefined = null;
let nul: null = undefined;
let voi: void = null;
strictNullChecks: true
을 설정하면 null과 undefined 서로의 타입까지 엄격하게 할당할 수 없게 할 수 있다.let voi: void = undefined; // Ok
function hello(msg: string): void {
console.log(`Hello ${msg}`);
}
const hi: void = hello('world'); // Hello world
console.log(hi); // undefined
function error(message: string): never {
throw new Error(message);
}
const never: [] = []; // 타입이 빈 배열일 수는 없다
never.push(3); // Error - TS2345: Argument of type '3' is not assignable to parameter of type 'never'.
// 초기화된 변수 `num`
let num = 12;
// 기본값이 설정된 매개 변수 `b`
function add(a: number, b: number = 2): number {
// 함수의 반환 값(`a + b`)
return a + b;
}
val은 string이나 number, isNumber는 boolean이다.
개발자는 isNumber가 앞에 'is'가 붙은 네이밍을 통해 숫자 여부를 판단하는 값임을 알 수 있다.
타입스크립트는 ‘isNumber’라는 이름만으로는 이 내용을 추론할 수 없다.
function someFunc(val: string | number, isNumber: boolean) {
if (isNumber) {
// toFixed 메서드: 소수 자릿수로 반올림 후 string으로 변환하는 메서드 - 앞에 number만 가능
val.toFixed(2); // Error - TS2339: ... Property 'toFixed' does not exist on type 'string'.
// val이 string인지 number인지 모르는 타입스크립트는 toFixed를 객체의 프로퍼티로 추론해버림
}
}
타입 단언으로 이 문제를 해결할 수 있다.
function someFunc(val: string | number, isNumber: boolean) {
if (isNumber) {
(val as number).toFixed(2); // as로 단언 후 괄호로 묶어줌
}
}
const response = await fetch('/api/user')
const result = (await response.json()) as UserInterface
function isUser(data: unknown): data is UserInterface {
return data && typeof data === 'objet' && 'name' in data // ...
}
const response = await fetch('/api/user')
const result = await response.json()
if (!isUser(result)) {
throw new Error(`${result}는 UserInterface가 아님`)
}
타입 단언은 마치 프로그래머가 타입스크립트에게 “나는 알고 있으니까 나를 믿어!”라고 알려주는 것과 같다.
매개 변수 x가 null이나 undefined일 수 있어서 에러가 발생하는 경우
function fnA(x: number | null | undefined) {
return x.toFixed(2); // Error - TS2533: Object is possibly 'null' or 'undefined'
}
if 조건문이나 타입 단언으로 해결할 수도 있지만,
마지막처럼 Non-null 단언 연산자를 이용하여 간단히 처리할 수 있다.
// if 조건문
function fnD(x: number | null | undefined) {
if (x) { // true이면 = 존재하면 = null이나 undefined가 아니면
return x.toFixed(2);
}
}
// 타입 단언
function fnB(x: number | null | undefined) {
return (x as number).toFixed(2);
}
// Non-null 단언 연산자
function fnE(x: number | null | undefined) {
return x!.toFixed(2); // 위의 내용을 간단하게 표현 가능
}
val의 타입을 매번 보장하기 위해 타입 단언을 여러 번 사용한 경우
function someFunc(val: string | number, isNumber: boolean) {
if (isNumber) {
(val as number).toFixed(2);
isNaN(val as number);
} else {
(val as string).split('');
(val as string).toUpperCase();
(val as string).length;
}
}
타입 가드를 사용하면 타입스크립트가 추론 가능한 타입의 특정 범위를 보장할 수 있다.
반환 값으로 NAME is TYPE
라는 술부를 가진 함수를 생성 후 적용
// val이 number일 경우 val의 타입을 number로 확정해주는 함수를 따로 선언
function isNumber(val: string | number): val is number {
return typeof val === 'number'; // typeof 사용
}
function someFunc(val: string | number) {
if (isNumber(val)) { // isNumber 함수 적용
val.toFixed(2);
isNaN(val);
} else {
val.split('');
val.toUpperCase();
val.length;
}
}
함수를 따로 만들지 않고 간편하게 처리하기
typeof
키워드 (number, string, boolean, symbol만 타입 가드로 인식)in
연산자 (우변 객체(val)가 any 타입이어야)instanceof
연산자// typeof
function someFuncTypeof(val: string | number) {
if (typeof val === 'number') {
val.toFixed(2);
isNaN(val);
} else {
val.split('');
val.toUpperCase();
val.length;
}
}
// in
function someFuncIn(val: any) {
if ('toFixed' in val) { // toFixed가 val에 있으면 val이 number라는 뜻
val.toFixed(2);
isNaN(val);
} else if ('split' in val) { // split이 val에 있으면 val이 string이라는 뜻
val.split('');
val.toUpperCase();
val.length;
}
}
// instanceof
class Cat {
meow() {}
}
class Dog {
woof() {}
}
function sounds(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow();
} else {
animal.woof();
}
}
개인적인 생각으로는 typeof
나 in
을 쓰는 게 간단할 것 같다.
기존에 만들어진 인터페이스에 내용 추가
interface IAnimal {
name: string
}
interface ICat extends IAnimal {
meow(): () => string
}
const catInfo: ICat = {
name: 'Kitty',
meow(): () => 'Meowwww'
}
interface IFullName {
firstName: string,
lastName: string
}
interface IFullName { // 같은 이름으로 추가 타입을 선언하면 확장됨
middleName: string
}
const fullName: IFullName = {
firstName: 'Tomas',
middleName: 'Sean',
lastName: 'Connery'
};
인터페이스로 class의 타입을 정의하는 경우 implements
키워드를 사용
interface IUser {
name: string,
getName(): string
}
class User implements IUser {
constructor(public name: string) {}
getName() {
return this.name;
}
}
const neo = new User('Neo');
neo.getName(); // Neo
type
키워드와 union 연산자로 둘 이상의 타입을 조합할 수 있다.type TUser = {
name: string,
age: number,
isValid: boolean
} | [string, number, boolean];
let userA: TUser = {
name: 'Neo',
age: 85,
isValid: true
};
let userB: TUser = ['Evan', 36, false];
두 가지 값을 인수로 받는 toArray 함수에서 number 타입 선언이 여러 번 이루어지고 있다.
여기서 string 타입을 넣으면 에러가 발생한다.
function toArray(a: number, b: number): number[] {
return [a, b];
}
toArray(1, 2);
toArray('1', '2'); // Error - TS2345: Argument of type '"1"' is not assignable to parameter of type 'number'.
string도 허용되도록 union을 이용해 인수 선언을 확장했다.
가독성이 떨어지고, 세 번째 호출의 경우 의도치 않게 number와 string 타입을 동시에 받게 되었다.
function toArray(a: number | string, b: number | string): (number | string)[] {
return [a, b];
}
toArray(1, 2); // Only Number
toArray('1', '2'); // Only String
toArray(1, '2'); // Number & String
함수명 옆에 제네릭 타입 <T>
를 작성하고 각 인수와 반환 값의 타입에도 T
를 할당했다.
T는 타입 변수(Type variable)로 개발자가 제공한 타입으로 변환된다.
기본적으로 타입 추론이 되지만, 세 번째처럼 인수에 서로 다른 타입이 들어올 경우에는 union 타입으로 각각 타입을 명시해줘야 한다.
function toArray<T>(a: T, b: T): T[] {
return [a, b];
}
toArray(1, 2);
toArray('1', '2');
toArray<string | number>(1, '2'); // union으로 타입 명시
interface ICountries {
KR: '대한민국',
US: '미국',
CP: '중국'
}
let country: keyof ICountries; // 'KR' | 'US' | 'CP'
country = 'KR'; // ok
country = 'RU'; // Error - TS2322: Type '"RU"' is not assignable to type '"KR" | "US" | "CP"'.
let country: ICountries[keyof ICountries]; // ICountries['KR' | 'US' | 'CP'] = '대한민국' | '미국' | '중국'
country = '대한민국';
country = '러시아'; // Error - TS2322: Type '"러시아"' is not assignable to type '"대한민국" | "미국" | "중국"'.
T는 타입(Type), U는 또 다른 타입, K는 key를 의미하는 약어
Partial<T>
Required<T>
Readonly<T>
Record<K, T>
Pick<T, K>
Omit<T, K>
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Omit<Todo, 'description'>;
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
createdAt: 1615544252770,
};
Exclude<T, U>
type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // a, b, c 중에 a 제거
type T0 = 'b' | 'c';
Extract<T, U>
null
과 undefined
만 제외하고 반환NonNullable<T>
Parameters<T>
ConstructorParameters<T>
ReturnType<T>
InstanceType<T>
ThisParameterType<T>
OmitThisParameter<T>
ThisType<T>
한눈에 보는 타입스크립트(updated)
[Typescript] 기존 React + js 프로젝트 ts로 바꾸기
타입스크립트에서 조심해야 할 습관
A difference between TypeScript Omit and exclude