🤔 why TS?
자바스크립트는 약한 타입 언어라고 표현할 수 있어 비교적 덜귀찮(?)고 유연하게 개발할 수 있는 환경을 제공하지만 런타임 환경에서 쉽게 에러가 발생할 수 있는 단점을 가진다.
이러한 자바스크립트에 강한 타입 시스템을 적용해 대부분의 에러를 컴파일 환경에서 코드를 입력하는 동안 체크할 수 있는 장점을 가지고 있기 때문에 요즘 많은 곳에서 타입스크립트를 사용하고 있는 것이 아닌가 싶다.
=> 에러사전 방지, 코드 가이드 및 자동완성(개발 생산성 향상)
정말 많은 도움이 되었고 정리가 너무 잘되어있는 heropy님의 블로그입니다
이 글은 위 블로그 정리내용을 다시 정리한 포스트 입니다
예제를 통해 초보, 입문자가 이해하기에도 쉽게 되어있어 좋은 참고자료로 썼습니다.
👀 heropy님 블로그 링크
작성한 내용이 컴파일러 옵션에 따라 어떻게 자바스크립트로 변환되는지 바로 확인가능한 사이트
👀 TypeScript Playground
캡틴판교님의 인강 - 타입스크립트 입문 - 기초부터 실전까지
포크 떠서 필기한 깃헙 레포
캡틴판교님의 교안
👀 타입 스크립트 핸드북
.ts 확장자를 가진 파일로 작성,
작성 후 타입스크립트 컴파일러를 통해 자바스크립트 파일로 컴파일하여 사용
타입 | 특징 |
---|---|
Boolean | 단순한 참(true)/거짓(false) 값 |
Number | 모든 부동 소수점 값을 사용 가능 |
String | 작은따옴표('), 큰따옴표(") 뿐만 아니라 ES6의 템플릿 문자열도 지원 |
Array | 순차적으로 값을 가지는 일반 배열로 string[], Array<string> 두가지 방법으로 선언 |
Tuple | 정해진 타입의 고정된 길이(length) 배열을 표현, 각 요소의 타입이 지정되어 있는 배열 형식 |
Enum | 열거형, 숫자 혹은 문자열 값 집합에 이름(Member)을 부여, 값의 종류가 일정한 범위로 정해져 있는 경우 유용, 기본적으로 0부터 시작하며 값은 1씩 증가, 특정 값들의 집합을 의미하는 자료형 |
Any | 모든 타입, 일반적인 자바스크립트 변수와 동일하게 어떤 타입의 값도 할당 가능, 외부 자원을 활용해 개발할 때 불가피하게 타입을 단언할 수 없는 경우, 유용 |
Unknown | 알 수 없는 타입, Unknown에는 어떤 타입의 값도 할당할 수 있지만, Unknown을 다른 타입에는 할당할 수 없음 |
Object | typeof 연산자가 "object"로 반환하는 모든 타입, 여러 타입의 상위 타입이기 때문에 그다지 유용하지 않음, 반복적인 사용을 원하는 경우, interface나 type을 사용하는 것을 추천 |
Null과 Undefined | 모든 타입의 하위 타입, 서로의 타입에도 할당 가능 |
Void | 일반적으로 값을 반환하지 않는 함수에서 사용 |
Never | 절대 발생하지 않을 값, 어떠한 타입도 적용할 수 없음 |
Union | 2개 이상의 타입을 허용하는 경우 ❗️ Union 타입 장점 - any 보다는 명시적임 : 타입가드시 메소드 등장 |
Intersection | 2개 이상의 타입을 조합하는 경우 |
Function | 화살표 함수를 이용해 타입을 지정 |
명시적으로 타입 선언이 되어있지 않은 경우, 타입스크립트는 타입을 추론
<타입스크립트가 타입을 추론하는 경우>
이를 활용해 모든 곳에 타입을 명시할 필요는 없음
타입 추론을 통해 판단할 수 있는 타입의 범주를 넘는 경우, 더 이상 추론하지 않도록 지시
-> 프로그래머가 타입스크립트보다 타입에 대해 더 잘 이해하고 있는 상황
DOM API 조작시 많이 사용됨
(num as number).toFixed(2);
Non-null 단언 연산자
let x: number | null | undefined
x!.toFixed(2);
특정 타입으로 타입의 범위를 좁혀나가는 과정
NAME is TYPE
형태의 타입 술부(Predicate)를 반환 타입으로 명시
function isNumber(val: string | number): val is number {
return typeof val === 'number';
}
❗️중요
여러 객체를 정의하는 일종의 규칙이며 구조
interface IUser {
name: string,
age: number,
isAdult: boolean
}
❗️ 타입 별칭과 인터페이스의 차이점
readonly
키워드를 사용하면 읽기 전용 속성을 정의interface IFunc {
(PARAMETER: PARAM_TYPE): RETURN_TYPE
}
implements
키워드를 사용interface INAME {
[INDEXER_NAME: INDEXER_TYPE]: RETURN_TYPE
}
인덱싱 가능 타입에서 keyof
를 사용하면 속성 이름을 타입으로 사용 가능, 타입의 개별 값에도 접근가능
인터페이스도 클래스처럼 extends
키워드를 활용해 상속가능
같은 이름의 인터페이스를 여러 개 만들기 가능: 기존에 만들어진 인터페이스에 내용을 추가하는 경우에 유용
type
키워드를 사용해 새로운 타입 조합하여 별칭(이름)을 부여 가능
❗️제네릭을 통해 사용 시점에 타입을 선언할 수 있는 방법을 제공: 타입을 인수로 받아서 사용한다고 이해하면 쉬움-> 함수 이름 우측에 <T>
를 작성해 시작
제네릭을 작성할때 extends
키워드를 사용하는 제약 조건을 추가 가능
interface MyType<T extends string | number> {
name: string,
value: T
}
extends
는 삼항 연산자 사용 가능infer
키워드를 통해 타입 변수의 타입 추론 여부를 확인 가능public
어디서나 자유롭게 접근 가능(생략 가능)protected
나와 파생된 후손 클래스 내에서 접근 가능private
내 클래스에서만 접근 가능?
키워드를 사용하여 타입을 선언할 때 선택적 매개 변수(Optional Parameter)를 지정가능,| undefined
를 추가하는 것과 동일모든 모듈에 대해 매번 직접 타입 선언을 작성하는 것은 매우 비효율적.
여러 사용자들의 기여로 만들어진 Definitely Typed을 사용가능
수 많은 모듈의 타입이 정의되어 있으며, 지속적으로 추가되고 있음
npm install -D @types/모듈이름
으로 설치해 사용
npm info @types/모듈이름
으로 검색하면 원하는 모듈의 타입 선언이 존재하는지 확인가능
타입 선언 모듈(@types/lodash)은 node_modules/@types경로에 설치되며,
이 경로의 모든 타입 선언은 Import를 통해 컴파일에 자동으로 포함
이름 | 설명 |
---|---|
Partial | TYPE 의 모든 속성을 선택적으로 변경한 새로운 타입을 반환 |
Required | TYPE 의 모든 속성을 필수로 변경한 새로운 타입을 반환 |
Readonly | TYPE 의 모든 속성을 읽기 전용으로 변경한 새로운 타입을 반환 |
Record | KEY 를 속성으로, TYPE 를 그 속성값의 타입으로 지정하는 새로운 타입을 반환 |
Pick | TYPE 에서KEY 로 속성을 선택한 새로운 타입을 반환 |
Omit | Pick과 반대로, TYPE 에서KEY 속성을 생략하고 나머지를 선택한 새로운 타입을 반환 |
Exclude | 유니언 TYPE1 에서 유니언 TYPE2 를 제외한 새로운 타입을 반환 |
Extract | TYPE1 에서 유니언 TYPE2 를 추출한 새로운 타입을 반환 |
NonNullable | 유니언 TYPE 에서 null 과 undefined 를 제외한 새로운 타입을 반환 |
Parameters | 함수 TYPE 의 매개변수 타입을 새로운 튜플 타입으로 반환 |
ReturnType | 함수 TYPE 의 반환 타입을 새로운 타입으로 반환 |
function sum(a: number, b: number): number {
return a + b;
}
// sum('10', '20'); // Error: '10'은 number에 할당될 수 없습니다.
// NOTE: 메소드 자동완성 가능
let result = sum(10, 20)
result.toLocaleString();
// NOTE: js doc 으로 타입스크립트 같이 사용가능 하긴함
// @ts-check
/**
* @param {number} a 첫번째숫자
* @param {number} b 두번째숫자
*/
// NOTE:
// $ sudo npm i typescript -g => 설치
// $ tsc index.ts => 컴파일
//NOTE: any: 모든 타입 가능 -> any로 먼저 설정해두고 하나씩 바꾸는게 좋음
let todoItems: ITodo[];
// NOTE: 인터페이스 사용 - 객체 및 함수
interface ITodo {
id: number,
title: string,
done: boolean,
}
interface ITodoFunc {
(index: number, todo?: ITodo): void;
}
let deleteTodo: ITodoFunc;
deleteTodo = function (index: number): void {
todoItems.splice(index, 1);
}
// api
function fetchTodoItems(): ITodo[] {
return [
{ id: 1, title: '안녕', done: false },
{ id: 2, title: '타입', done: false },
{ id: 3, title: '스크립트', done: false },
];
}
// crud methods
function fetchTodo(): ITodo[] {
return fetchTodoItems();
}
//NOTE: void: 리턴 값이 없는 친구들
function addTodo(todo: ITodo): void {
todoItems.push(todo);
}
function completeTodo(index: number, todo: ITodo): void {
todo.done = true;
todoItems.splice(index, 1, todo);
}
// business logic
function logFirstTodo(): ITodo {
return todoItems[0];
}
function showCompleted(): ITodo[] {
return todoItems.filter((item: ITodo) => item.done);
}
// TODO: 아래 함수의 내용을 채워보세요. 아래 함수는 `addTodo()` 함수를 이용하여 2개의 새 할 일을 추가하는 함수입니다.
function addTwoTodoItems(item1: ITodo, item2: ITodo): void {
// addTodo() 함수를 두 번 호출하여 todoItems에 새 할 일이 2개 추가되어야 합니다.
addTodo(item1);
addTodo(item2);
}
// NOTE: 유틸 함수
function log(): void {
console.log(todoItems);
}
todoItems = fetchTodoItems();
const todo1 = { id: 4, title: '네번째', done: false };
const todo2 = { id: 5, title: '다섯번째', done: false };
addTwoTodoItems(todo1, todo2);
log();
// NOTE: 인터페이스 인덱싱
interface IStringArray {
[index: number]: string;
}
let arr: IStringArray = ['a', 'b'];
arr[0] = 'c';
console.log(arr); // ['c', 'b']
// arr[0] = 10; -> 에러
interface IStringRegExDict {
[key: string]: RegExp;
}
let obj: IStringRegExDict = {
cssFile: /\.css$/,
// jsFile: 123, -> 에러
}
obj['jsFile'] = /\.js$/;
Object.keys(obj).forEach(key => {
console.log(key); // key => 자동 추론
})
// NOTE: 인터페이스 확장
interface Person {
name: string;
}
interface Developer extends Person {
skill: string;
}
const seoYoung: Developer = {
name: 'um',
skill: 'js',
}
function Person(name, age) {
this.name = name;
this.age = age;
}
const hulk = new Person('Banner', 33);
//ES6 + 타입스크립트
class Person2 {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const capt = new Person2('Steve', 100);
/** NOTE: 타입 별칭과 인터페이스의 차이점
* 타입의 확장 가능 / 불가능 여부
* 인터페이스는 확장이 가능한데 반해 타입 별칭은 확장이 불가능
* 따라서, 가능한한 type 보다는 interface로 선언해서 사용하는 것을 추천
*/
// #1
// function sum(a: number, b:number) {
// return a + b;
// }
type SumParameter = number;
function sum(a: SumParameter, b: SumParameter) {
return a + b;
}
// #2
type Student = {
name: string;
age: number;
};
function getPerson(): Student {
// ...
return {
name: 'seoYoung',
age: 28
}
}
// #3
type Hero = {
skill: string;
}
const captain: Hero = {
skill: 'throwing a shield'
}
// function logMessage(value: string) {
// console.log(value);
// }
// function logMessage(value: number) {
// console.log(value);
// }
// function logMessage(value: any) {
// console.log(value);
// }
// # NOTE: Union 타입 문법 - `any` 보다는 명시적임 : 타입가드시 메소드 등장
// function logMessage(value: string | number) {
// console.log(value);
// }
function logMessage(value: string | number) {
if (typeof value === 'string') {
value.toLocaleUpperCase();
}
if (typeof value === 'number') {
value.toLocaleString();
}
throw new TypeError('value must be string or number')
}
// # Intersection 타입 문법
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
// NOTE: Union 타입 => 어떤 것이 들어올지 모르므로 공통된 속성만 접근가능
function askSomeone(someone: Developer | Person) {
someone.name; // O
// someone.age; // X
}
askSomeone({ name: '서영', skill: 'js' });
askSomeone({ name: '서영', age: 20 });
// NOTE: Intersection 타입 => 조합된 모든 속성만 접근가능
function askSomeone2(someone: Developer & Person) {
someone.name; // O
someone.age; // O
someone.skill; // O
}
askSomeone2({ name: '서영', age: 20, skill: 'js' });
// EXAMPLE: enum 예제
enum Answer {
Yes = 'Y',
No = 'N',
}
function respond(message: Answer): void {
console.log(message);
}
respond(Answer.Yes);
//respond('Y'); - 에러
// NOTE: 런타임 시점에서의 이넘 특징: 런타임시에 실제 객체 형태로 존재
enum E {
X, Y, Z
}
function getX(obj: { X: number }) {
return obj.X;
}
getX(E); // 이넘 E의 X는 숫자이기 때문에 정상 동작
// NOTE: 컴파일 시점에서의 이넘 특징: 일반적으로 keyof를 사용해야 되는 상황에서는 대신 keyof typeof를 사용
enum LogLevel {
ERROR, WARN, INFO, DEBUG
}
// 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log('Log level key is: ', key);
console.log('Log level value is: ', num);
console.log('Log level message is: ', message);
}
}
printImportant('ERROR', 'This is a message');
function getNumber(value: number) {
return value;
}
function getArray(value: string[]) {
return value;
}
// 제네릭 기본 문법 - 함수
function getValue<T>(value: T): T {
return value;
}
// getValue<string>('hi').toLocaleUpperCase();
getValue('hi').toLocaleUpperCase();
// getValue<number>(100).toLocaleString();
getValue(100).toLocaleString();
// 제네릭 기본 문법 - 인터페이스
interface Dev<T> {
name: string;
age: T;
}
const tony: Dev<number> = { name: 'tony', age: 100 };
// 제네릭 타입 제한 - 구체적인 타입
function getNumberAndArray<T>(value: T[]): T[] {
value.length; // O
return value;
}
interface IHasLength {
length: number;
}
// NOTE: 속성가진 인터페이스 확장하여 사용
function getNumberAndArray2<T extends IHasLength>(value: T): T {
value.length;
return value;
}
// 제네릭 타입 제한 - keyof
interface ShoppingItems {
name: string;
price: number;
address: string;
stock: number;
}
function getAllowedOptions<T extends keyof ShoppingItems>(option: T): T {
if (option === 'name' || option === 'address') {
console.log('option type is string');
return option;
}
if (option === 'price' || option === 'stock') {
console.log('option type is number');
return option;
}
}
// getAllowedOptions('nothing');
const a = getAllowedOptions('name');
a.toUpperCase(); // Name
// EXAMPLE: 제네릭 이용 예제
interface DropdownItem<T> {
value: T;
selected: boolean;
}
const emails: DropdownItem<string>[] = [
{ value: 'aaa@naver.com', selected: true },
{ value: 'bbb@naver.com', selected: false },
{ value: 'ccc@naver.com', selected: false }
]
const numProducts: DropdownItem<number>[] = [
{ value: 1, selected: true },
{ value: 2, selected: false },
{ value: 3, selected: false }
]
// 유니온 타입
function createDropdownItem(item: DropdownItem<string> | DropdownItem<number>) {
//..
}
// 유니온 타입을 제네릭으로 변경
function createDropdownItem2<T>(item: DropdownItem<T>) {
//..
}
emails.forEach((email) => createDropdownItem(email))
numProducts.forEach((prd) => createDropdownItem(prd))
emails.forEach((email) => createDropdownItem2<string>(email))
numProducts.forEach((prd) => createDropdownItem2<number>(prd))
// 인터페이스
interface IDeveloper {
name: string;
skill: string;
}
interface IPerson {
name: string;
}
//NOTE: 타입호환 - 작은타입에 큰타입을 넣는 것은 가능
var developer: IDeveloper;
var person: IPerson;
// developer = person; // X
person = developer; // O
// 함수
var _add = function(a: number) {
// ...
}
var _sum = function(a: number, b: number) {
// ...
}
_sum = _add; // O
// _add = _sum; // X
// 유니온 타입
var c: IDeveloper | IPerson;
var d: IPerson | string;
// c = d; // X
d = c; // O
// 제네릭
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x
interface NotEmpty<T> {
data: T;
}
let x1: NotEmpty<number>;
let y1: NotEmpty<string>;
// x1 = y1; // Error, because x and y are not compatible
interface PhoneNumberDictionary {
[phone: string]: {
num: number;
};
}
interface Contact {
name: string;
address: string;
phones: PhoneNumberDictionary;
}
enum Place {
Home = 'home',
Office = 'office',
Studio = 'studio',
}
// api
// NOTE: Promise<제네릭>
function fetchContacts(): Promise<Contact[]> {
const contacts: Contact[] = [
{
name: 'Tony',
address: 'Malibu',
phones: {
home: {
num: 11122223333,
},
office: {
num: 44455556666,
},
},
},
{
name: '마동석',
address: '서울시 강남구',
phones: {
home: {
num: 213423452,
},
studio: {
num: 314882045,
},
},
},
];
return new Promise(resolve => {
setTimeout(() => resolve(contacts), 2000);
});
}
// main
class AddressBook {
contacts: Contact[] = [];
constructor() {
this.fetchData();
}
fetchData(): void {
fetchContacts().then(response => {
this.contacts = response;
});
}
/* TODO: 아래 함수들의 파라미터 타입과 반환 타입을 지정해보세요 */
findContactByName(name: string): Contact[] {
return this.contacts.filter(contact => contact.name === name);
}
findContactByAddress(address: string): Contact[] {
return this.contacts.filter(contact => contact.address === address);
}
findContactByPhone(phoneNumber: number, phoneType: Place): Contact[] {
return this.contacts.filter(
contact => contact.phones[phoneType].num === phoneNumber
);
}
addContact(contact: Contact): void {
this.contacts.push(contact);
}
displayListByName(): string[] {
return this.contacts.map(contact => contact.name);
}
displayListByAddress(): string[] {
return this.contacts.map(contact => contact.address);
}
/* ------------------------------------------------ */
}
const addressBook = new AddressBook();
const found = addressBook.findContactByPhone(11122223333, Place.Office)
📃 type-def.ts
interface PhoneNumberDictionary {
[phone: string]: {
num: number;
};
}
interface Contact {
name: string;
address: string;
phones: PhoneNumberDictionary;
}
enum Place {
Home = 'home',
Office = 'office',
Studio = 'studio',
}
export {
Contact,
Place
}
📃 index.ts
import { Contact, Place } from './type-def'
//..