※ VSCode에서 타입스크립트 실행방법
npm install -g ts-node
ts-node index.ts
실행object와 reference형태가 아닌 실제 값을 저장하는 자료형
// string
let str: string = "hi"
// boolean
let isSucceed: boolean = true
// number
let decimal: number = 6
// null
let n: null = null
// undefined
let u: undefined = undefined
객체, 배열, 함수와 같은 Object 형식의 타입
string, boolean, number, null, undefined 제외한 나머지
// 예시1
function create(o: object): void{}
create({prop: 0}) // 성공
create([1,2,3]) // 성공
create("string") // error
create(42) // error
// 예시2
let arr: number[] = [1,2,3]
// 예시3
let arr: Array<number> = [1,2,3]
타입스크립트에서 개발자의 편의를 위해 추가로 제공하는 타입
// tuple (길이와 각 요소의 타입이 정해진 배열을 저장하는 타입)
let arr: [string, number] = ["Hi", 6]
// enum (특정 상수들의 집합을 저장하는 타입)
enum Car {Bus, Taxi, Suv}
let bus: Car = Car.Bus
let bus: Car = Car[0]
enum Car {Bus=1, Taxi=2, Suv=3}
let taxi: String = Car[2]
enum Car {Bus=2, Taxi, Suv}
let taxi: String = Car[3]
// any (모든 타입을 저장 가능)
let str: any = "hi"
let num: any = 10
// void (함수에서 반환 값이 없는 타입) (변수에는 undefined와 null만 할당)
let unknown: void = undefined
function sayHi(): void {
console.log("hi")
}
// never (항상 오류를 발생시키거나 절대 반환하지 않는 타입) (종료되지 않는 함수)
function neverEnd(): never {
while (true) {}
}
function error(message: string): never { // 항상 오류를 발생시키는 함수 정의
throw new Error(message)
}
// 1
enum Car {
BUS = "bus",
TAXI = "taxi",
SUV = "suv"
}
const taxi:Car = Car.TAXI
console.log(taxi); // taxi
// 2
enum Car {
BUS,
TAXI,
SUV
}
const taxi:Car = Car.TAXI
console.log(taxi); // 1
interface Todo {
title: string
description: string
}
function updateTodo(obj: Partial<Todo>) {
return obj;
}
// title or (title & description) or description 가능
const result = updateTodo({
title: "title",
})
console.log(result); // { title: 'title' } 출력
interface User {
id: number;
name: string;
age: number;
}
let admin: Partial<User> = {
id: 1,
name: "Bob",
}
// ↕ 같은 의미
interface User {
id?: number; // ? : 빈칸이면 undefined로 들어감
name?: string;
age?: number;
}
interface User {
id: number;
name: string;
age?: number; // ? : 빈칸이면 undefined
}
let admin1: User = { id: 1, name: "Sng" } // error 없음
let admin2: Required<User> = {
id: 1,
name: "Sng"
} // error 발생 (age까지 모두 있어야함)
interface User {
id: number;
name: string;
age?: number;
}
let admin: Readonly<User> = {
id: 1,
name: "Bob",
}
admin.id = 4 // 변경하려고 하면 Error 발생
interface Score {
"1": "A" | "B" | "C";
"2": "A" | "B" | "C";
"3": "A" | "B" | "C";
}
const score: Score = {
1: "A",
2: "B",
3: "C",
};
console.log(score); // { '1': 'A', '2': 'B', '3': 'C' }
// ↕
type Grade = "1" | "2" | "3";
type Score = "A" | "B" | "C" | "D";
const score: Record<Grade, Score> = {
1: "A",
2: "B",
3: "C",
};
console.log(score); // { '1': 'A', '2': 'B', '3': 'C' }
interface User {
id: number;
name: string;
age: number;
}
function isValid(user: User) {
const result: Record<keyof User, boolean> = { // keyof User: User의 키
id: user.id > 0,
name: user.name !== "",
age: user.age > 0, // 만약 age?: number로 정의하면, 이곳에서 에러가 난다. ('user.age' is possibly 'undefined'.)
};
return result;
}
const test: User = { id: -1, name: "Lee", age: 19 };
console.log(isValid(test)); // { id: false, name: true, age: true }
interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact"; // keys
const x: Record<Page, PageInfo> = {
about: { title: "about" },
contact: { title: "contact" },
home: { subtitle: "home" }, // Error! 'PageInfo' 형식에 'subtitle'이(가) 없습니다
main: {title: 'home'} // Error! 'Record<Page, PageInfo>' 형식에 'main'이(가) 없습니다.
};
interface Todo {
title: string
description: string
completed: boolean
}
// Omit을 이용해 description 프로퍼티를 제외해봅니다.
type TodoPreview = Omit<Todo,"description"> // 반드시 2개의 인자
const todo: TodoPreview = {
title: "Clean room",
completed: false,
}
console.log(todo) // { title: 'Clean room', completed: false }
interface Todo {
title: string;
description: string;
score: number;
completed: boolean;
}
// Pick을 이용해 title 프로퍼티를 포함해봅니다..
type TodoPreview = Pick<Todo, "description" | "title" | "score">; // 반드시 2개의 인자
const todo: TodoPreview = {
title: "Clean room",
description: "des",
score: 10
};
console.log(todo); // { title: 'Clean room', description: 'des', score: 10}
type T0 = Exclude<"a" | "b" | "c", "a">;
// "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// "c"
type T2 = Exclude<string | number | (() => void), Function>;
// string | number
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// "a" (a,b,c 중에 a,f를 추출했고 f는 존재하지 않으니 a 타입만 구성
type T1 = Extract<string | number | (() => void), Function>;
// () => void (Function 타입만 추출)
type T0 = NonNullable<string | number | undefined>;
// string | number (undefined는 제외)
type T1 = NonNullable<string[] | null | undefined>;
// string[] (null, undefined는 제외)
// js 함수 선언식
function world(name) {
return `Hello ${name}`
}
// js 함수 표현식
let world = function(name) {
return `Hello ${name}`
}
// ts 함수 선언식
function world(name: string): string{
return `Hello ${name}`
}
// ts 함수 표현식
let world = function(name: string): string{
return `Hello ${name}`
}
// js 화살표 함수 표현식
let world = (name) => {
return `Hello ${name}`
}
// js 단축형 화살표 함수 표현식
let world = (name) => `Hello ${name}`
// ts 화살표 함수 표현식
let world = (name: string): string => {
return `Hello ${name}`
}
// ts 단축형 화살표 함수 표현식
let world = (name: string): string => `Hello ${name}`
function buildName(firstName: string, lastName: string) {
return firstName + "" + lastName;
}
let result1 = buildName("Bob"); // Error: Expected 2 arguments, but got 1
let result2 = buildName("Bob", "Adams");
let result3 = buildName("Bob","Adams","Sr."); // Error: Expected 2 arguments, but got 3
function buildName(firstName: string, lastName?: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // Bob undefined
let result2 = buildName("Bob", "Adams"); // Bob Adams
let result3 = buildName("Bob","Adams","Sr."); // Error: Expected 2 arguments, but got 3
// lastWord를 Default Parameter 형식으로 수정해주세요.
function say(firstWord: string, lastWord: string = "타입스크립트") {
return firstWord + " " + lastWord;
}
let result1 = say("엘리스"); // 엘리스 타입스크립트
let result2 = say("엘리스", undefined); // 엘리스 타입스크립트
let result3 = say("엘리스", "파이썬"); // 엘리스 파이썬
let result4 = say("엘리스", "파이썬", "자바"); // Error: Expected 2 arguments, but got 3
function makeWord(firstChar: string, ...restOfChar: string[]) { // string[] : 배열 Type
return restOfChar
}
let word = makeWord("타", "입", "스", "크", "립", "트")
console.log(word) // [ '입', '스', '크', '립', '트' ]
클래스 문법
class
키워드를 이용해 클래스를 만들 수 있습니다.this
키워드를 사용합니다.new
키워드를 이용해 클래스의 인스턴스를 생성합니다.클래스 구성
this.
를 앞에붙이면 클래스의 멤버를 의미class Person {
name: string
constructor(name: string) {
this.name = name
}
say() {
console.log("my name is " + this.name)
}
}
const person = new Person("엘리스")
person.say() // my name is 엘리스
public
public
으로 선언되지만, 명시적으로 표시해도 됩니다.protected
priavte
※ default는 없음
class Animal {
public name: string;
constructor(theName: string) {
this.name = theName;
}
}
const ans = new Animal("Cat").name
console.log(ans) // Cat
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
const ans = new Animal("Cat").name
console.log(ans) // Error!
// 'name' 속성은 'Animal' 클래스 내에서만 액세스 가
class Animal {
protected name: string
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
makeSound() {
console.log(this.name + " 멍멍!!")
}
}
const dog = new Dog("진돗개")
dog.makeSound() // 진돗개 멍멍!!
class의 속성에 직접 접근하는 것을 막고, getter
와 setter
함수를 사용해 값을 받아오거나 수정한다. 속성에 직접 접근해 수정하면 데이터 무결성이 깨질 수 있다.
클래스의 멤버에 잘못된 값을 넣어 에러를 발생시키는 것을 막기 위해 이용합니다. 예를 들어 사람 클래스에 나이 속성이 있다고 할 때, 나이는 무조건 0보다 크기 때문에 getter / setter를 설정하여 0 이하의 값이 들어오는 것을 방지할 수 있습니다.
이렇게 멤버에 직접적으로 접근하지 못하도록 하는 것을 캡슐화라고 합니다.
class Person {
private _name: string;
get name() {
return this._name;
}
set name(name: string) {
if (name.length > 10) {
throw new Error("name too long");
}
this._name = name;
}
}
let person = new Person();
console.log(person.name); // undefined (아직 정의되지 않았기 때문)
person.name = "june";
console.log(person.name); // june
person.name = "junejunejunejunejune"; // throw Error
속성을 읽기 전용으로 설정해 변경할 수 없게 만듭니다. 그래서 선언될 때나 생성자에 값을 설정한 이후에는 수정이 불가능한 속성입니다.
변경될 경우가 없는 상수에 해당 키워드를 이용합니다. 비슷한 기능을 하는 const
가 있는데 const
는 변수 참조를 위한 것이고 readonly
는 속성(프로퍼티)을 위한 것입니다.
class Person {
readonly age: number = 20; // 선언 초기화
constructor(age: number) {
this.age = age;
}
}
let person = new Person(10); // 생성자 초기화
person.age = 30; // Error! 읽기 전용 속성이므로 'age'에 할당할 수 없습니다.
객체마다 할당되지 않고 클래스의 모든 객체가 공유하는 멤버인 전역 멤버를 선언할 때 사용
static은 클래스 인스턴스가 아닌 클래스 이름을 이용해 접근이 가능
즉, 클래스의 인스턴스를 생성하지 않아도 호출 할 수 있다.
class Grid {
static origin = { x: 0, y: 0 };
calculateDistance(): void {
console.log(Grid.origin.x * Grid.origin.y);
}
}
const grid = new Grid();
Grid.origin = { x: 3, y: 3 }; // 클래스명(Grid)을 이용해야 변경 가능
grid.calculateDistance(); // "9" 출력
class Foo {
static instanceCounter = 0;
constructor() {
// 생성자가 호출될 때마다 카운터를 1씩 증가시킨다.
Foo.instanceCounter++;
}
}
console.log(Foo.instanceCounter); // 2
let foo1 = new Foo();
console.log(foo1.instanceCounter); // error
interface 사용X VS interface 사용O
// interface 사용 X
function sayMyJob(obj: {job: string}) {
console.log(obj.job);
}
const developer = { job: "개발자" }
sayName(developer) // 개발자
// interface 사용 O
interface Person {
job: string;
}
function sayMyJob(obj: Person) {
console.log(obj.job);
}
const developer = { // 또는 const developer: Person = {job: "개발자"}
job: "개발자",
};
sayMyJob(developer); // 개발자
1️⃣ function type
// source와 subString이 인자, return값은 boolean으로 설정
interface SearchFunc {
(source: string, subString: string): boolean;
}
// 변수로 직접 함수 값이 할당되었기 때문에 인수 타입 생략가능
let mySearch: SearchFunc;
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
};
// return값이 만약 string형식으로 반환된다면 에러 발생
mySearch = function (src, sub) {
let result = src.search(sub);
return "string";
};
2️⃣ class type
interface Animal {
makeSound(): void
}
// Dog class에 Animal interface를 implements합니다.
// 따라서 interface가 가지고 있는 함수를 class에서 구현해야 합니다.
class Dog implements Animal{
makeSound(): void { console.log('멍멍')}
}
const dog = new Dog();
dog.makeSound(); // 멍멍
3️⃣ interface 확장
interface Animal {
makeSound(): void
}
// Dog 인터페이스에 Animal 인터페이스를 확장하세요.
interface Dog extends Animal{
run(): void
}
// BullDog class는 Dog interface를 implements했기 때문에,
// interface가 가지고 있는 함수를 class에서 구현시켜야합니다.
class BullDog implements Dog {
makeSound(): void {
console.log('멍멍')
}
run() : void {
console.log('달리기')
}
}
const bullDog = new BullDog();
bullDog.makeSound(); // 멍멍
bullDog.run(); // 달리기
4️⃣ hybrid type
Counter
인터페이스는 함수로서 호출도 가능하고, 여러 프로퍼티도 가지고 있습니다.interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
getCounter()
함수에 나오는 as
는 타입 단언을 위한 문법으로, 말 그대로 타입을 컴파일러가 추론하지 않도록 프로그래머가 직접 지정하는 것입니다.
5️⃣ optional properties
interface Config {
color: string;
width?: number;
}
function createSqure(config: Config): { color: string; area: number } {
return {
color: config.color,
area: 100 * (config.width ? config.width : 1),
};
}
const config = {
color: "red",
};
console.log(createSqure(config)); // { color: 'red', area: 100 }
pay
메소드 구현 변경이 필요하다. 이러면 확장성이 떨어지며, OCP(개방 폐쇄)원칙에 위배됨. ※ OCP원칙 : 확장에는 열려있고, 수정에는 닫혀있어야함implements
하되 현금으로 결제하는 메소드, 카드로 결제하는 메소드를 구분해서 구현하는 것입니다. → 또다른 결제 수단이 추가되었더라도 vendingmachone 안의 pay메소드를 수정할 필요없이 또다른 클래스만 구현해서 값을 변경해준다interface PaymentStrategy {
pay(): void;
}
// 각 클래스의 `pay()` 메소드를 호출했을 때 cash pay, card pay가 출력되어야 합니다.
class CashPaymentStrategy implements PaymentStrategy {
pay(): void {
console.log("cash pay");
}
}
class CardPaymentStrategy implements PaymentStrategy {
pay(): void {
console.log("card pay");
}
}
class VendingMachine {
private paymentStrategy: PaymentStrategy;
// VendingMachine 인스턴스 생성될 때 기본적으로 카드 결제 전략을 사용하게 된다.
constructor() {
this.paymentStrategy = new CardPaymentStrategy(); // 초기값 설정
}
setPaymentStrategy(paymentStrategy: PaymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
pay() {
this.paymentStrategy.pay();
}
}
const vendingMachine = new VendingMachine();
vendingMachine.setPaymentStrategy(new CashPaymentStrategy());
vendingMachine.pay(); // cash pay
vendingMachine.setPaymentStrategy(new CardPaymentStrategy());
vendingMachine.pay(); // card pay
만약 기본으로 현금 결제 전략을 사용하고 싶다면, CashPaymentStrategy로 바꾼다.
constructor() {
this.paymentStrategy = new CashPaymentStrategy();
}
제네릭이란?
제네릭을 사용하는 이유
echo
함수에서 제네릭을 사용하지 못했다면 타입별로 하나의 함수를 만들어야 할 것입니다. 제네릭을 사용하면 하나만 선언해도 여러 타입에서 동작이 가능합니다.any
타입을 이용해야 합니다.function echo2(text: any): any {
return text;
}
any
타입은 컴파일 시 타입을 체크하지 않기 때문에 입력된 타입에 대한 메소드의 힌트를 사용할 수 없고, 컴파일 시 발견되는 에러를 발견할 수 없게됩니다.제네릭을 이용한 함수 생성
function echo<T>(text: T): T {
return text;
}
// 정의가 아닌 생성 시점에 명시하기 때문에 여러 타입으로 정의할 수 있음
console.log(echo**<string>**("hi"));
console.log(echo**<number>**(10));
console.log(echo**<boolean>**(true));
function sort<T>(items: T[]): T[] {
return items.sort();
}
const nums: number[] = [1, 2, 3, 4];
const chars: string[] = ["a", "b", "c", "d"];
sort<number>(nums);
sort<string>(chars);
제네릭을 사용한 클래스 생성
class Queue<T> {
protected data: Array<T> = [];
push(item: T) {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
const numberQueue = new Queue<number>(); // Type을 Number만 가능하게 정의
numberQueue.push(0);
numberQueue.push("1"); // Error! string' 형식의 인수는 'number' 형식의 매개 변수에 할당될 수 없습니다
numberQueue.push(+"1"); // 실수를 사전 인지하고 수정할 수 있음
|
를 이용해 선언한다. 제네릭과 마찬가지로 여러 가지 타입을 다룰 수 있다.const printMessage = (message: string | number) => {
return message;
}
const message1 = printMessage(1234);
const message2 = printMessage("hello world!");
message1.length; // Error! number에는 length 속성이 존재하지 않기 때문에 에러 발생
message2.length;
위 코드를 제네릭으로 바꿔보자!
const printMessage = <T>(message: T) => {
return message;
};
const message1 = printMessage<string>("hello world!");
console.log(message1.length); // 12
원치 않은 속성에 접근하는 것을 막기 위해 Generic에 제약조건을 사용
extends
키워드를 이용해 제약조건(Constraints)을 사용할 수 있습니다.const printMessage = <T extends string | number>(message: T): T => {
return message;
}
printMessage<string>("1")
printMessage<number>(1)
printMessage<boolean>(false) // Error! 제약조건을 벗어나는 타입을 선언하면 에러가 발생
keyof
두 객체를 비교할 때 사용
const getProperty = <T extends object, U extends keyof T>(obj: T, key: U) => {
return obj[key]
}
getProperty({a:1, b:2, c:3}, "a"); // 1
getProperty({a:1, b:2, c:3}, "z"); // Error!
U의 값인 ‘z’ 가 Generic T의 키 값 중 존재하지 않기 때문에 오류가 발생
// 공통
interface Car {
drive(): void;
park(): void;
}
class Bus implements Car {
drive(): void {}
park(): void {
console.log("버스 주차");
}
}
class Taxi implements Car {
drive(): void {}
park(): void {
console.log("택시 주차");
}
}
class Suv implements Car {
drive(): void {}
park(): void {
console.log("Suv 주차");
}
}
제너릭 사용X
class CarFactory {
static getInstance(type: String): Car {
// car 종류를 추가할 때마다 case문을 추가해야하는 단점이 있음
switch (type) {
case "bus":
return new Bus();
default:
return new Taxi();
}
}
}
const bus = CarFactory.getInstance("bus")
const taxi = CarFactory.getInstance("taxi")
console.log(bus.park());
console.log(taxi.park());
// 버스 주차
// undefined
// 택시 주차
// undefined
제너릭 사용O
class CarFactory {
static getInstance<T extends Car>(type: { new (): T }): T {
return new type();
}
}
const bus = CarFactory.getInstance(Bus)
const taxi = CarFactory.getInstance(Taxi)
console.log(bus.park());
console.log(taxi.park());
// 버스 주차
// undefined
// 택시 주차
// undefined
제너릭을 이용해 getInstance
메소드가 여러 서브 클래스를 타입으로 가질 수 있게, 즉 타입을 반환만 할 수 있게 만들고 타입을 넘겨주도록 작성한다면 새로운 타입이 추가되어도 getInstance
를 수정할 필요가 없게 됩니다.