[TS] TypeScript 기본 개념 정리

Janet·2023년 6월 22일
0

TypeScript

목록 보기
8/8

Type 생성 및 명시

// 1. 타입 일회성으로 사용할 때: ex) 변수 선언 내부에서 object 타입 지정
const playerJames: {
  name: string;
  age?: number;
} = {
  name: 'james',
  age: 10,
};

// 2. 해당 타입 자주 사용해야할 때: 재사용 가능 타입 선언하기
type Nickname = string;
type Player = {
  readonly name: string; // readonly: 읽기 전용 속성
  age?: number; // ?: optional type
  nickname?: Nickname; // 타입 안에 타입 지정
};

// 2번의 선언한 타입 적용시키기
const jiyaho: Player = {
  name: 'jiyaho',
  age: 12,
  nickname: '',
};

// 함수의 인수(argument)에 타입 지정: ex) function funcName (argument: type) {}
// 반환값(return)에 타입 지정: ex) function funcName (): type {}
function playerMaker(name: string): Player {
  return {
    name,
  };
}

// Arrow function 적용 예제
const example = (name: string): Player => ({ name });

const nico = playerMaker('nico');
nico.age = 12;
// nico.name = 'nico'; // Player type에서 name의 속성이 읽기전용(readonly)이므로 업데이트 불가

// Tuple: 배열 안의 요소들의 타입을 순서대로 명시하고 싶을 때
const profile: [string, number, boolean] = ['name', 20, true];

// any는 타입 보호 장치를 비활성화 시킨다. (어느 타입이나 허용)
const a: any[] = [1, 2, 3];
const b: any = true;
console.log(a + b); // '1,2,3true'

// unknown type: 변수 타입을 미리 알지 못할때 사용
let c: unknown;
if (typeof c === 'number') {
  let d = c + 1;
}

// void: 아무것도 리턴하지 않는 함수를 대상으로 사용
function hello() {
  console.log('x');
}

// never: 함수가 절대 리턴하지 않을 때 발생. 함수에서 예외가 발생할 때.
// ex) 리턴하지 않고 에러를 발생시키는 함수
function hi(): never {
  throw new Error('xxx');
}
// ex) 명시한 타입에 해당되지 않는 예외의 경우
function never(name: string | number) {
  if (typeof name === 'string') {
    name;
  } else if (typeof name === 'number') {
    name;
  } else {
    name; // never
  }
}

Call signature, Overloading

// 함수의 Call signature 만들기: 타입을 미리 생성하여 함수에 가져와 적용
type Add = (a: number, b: number) => number;
const add: Add = (a, b) => a + b;

// overloading: 여러 개의 call signatures가 있는 함수
type Exam = {
  (a: number, b: number): number;
  (a: number, b: string): number;
};
const exam: Exam = (a, b) => {
  if (typeof b === 'string') return a;
  return a + b;
};

// overloading 시 파라미터의 개수가 다른 경우 optional type으로 명시하여 에러 해결
type Exam1 = {
  (a: number, b: number): number;
  (a: number, b: number, c: number): number;
};
const exam1: Exam1 = (a, b, c?: number) => {
  if (c) return a + b + c;
  return a + b;
};

Polymorphism(다형성), Generic

// Polymorphism(2개 이상의 구조)일 때 Generics를 사용하여 아래와 같이 적용 가능
// Generic: 많은 타입이 필요 시, 직접 명시하지 않고 타입스크립트가 추론하게 하여 알맞은 타입을 적용시킴
// 제네릭은 '선언' 시점이 아니라 '생성' 시점에 타입을 명시하여,
// 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법
type SuperPrint = <T>(arr: T[]) => void; // 제네릭은 통상적으로 <T> 등으로 사용
type SuperPrint1 = <T, V>(a: T[], b: V) => T; // 제너릭스 여러 개 사용 가능
const superPrint: SuperPrint = (arr) => {
  arr.forEach((i) => console.log(i));
};
superPrint([1, 2, 3]);
superPrint([true, false]);
superPrint(['a', 'b', 'c']);
superPrint([1, 2, true, false, 'hello']);

// Call signature 별도로 만들지 않고 함수에 바로 Generic 적용시키기
function superPrint1<T>(a: T[]) {
  return a[0];
}
const A = superPrint1([1, 2, 3]);

// 제네릭 재사용 및 확장
type Member<E> = {
  name: string;
  extraInfo: E;
};
type NicoExtra = {
  favFood: string;
};
type NicoMember = Member<NicoExtra>;
const nicolas: NicoMember = {
  name: 'nico',
  extraInfo: {
    favFood: 'kimchi',
  },
};
const tom: Member<null> = {
  name: 'tom',
  extraInfo: null,
};

// 배열에 넘버 타입의 제네릭 적용
type Arr = Array<number>;
type Arr1 = number[]; // 위와 같음
let arr: Arr = [1, 2, 3];
let arr1: Arr1 = [1, 2, 3];

Class, Abstract class

// 접근 제어자(필드의 보호 등급을 설정) 3가지
// private: 선언한 클래스 내에서만 접근 가능
// protected: 선언한 클래스 내, 상속받은 클래스 내에서 접근 가능
// public: 선언한 클래스 내, 상속받은 클래스 내, 인스턴스에서도 접근 가능
// 만약 접근 제어자를 명시하지 않은 경우 기본 속성값은 public이다.

// 추상 클래스는 다른 클래스가 상속받을 수 있는 클래스이다.
// 하지만 추상 클래스는 직접 새로운 인스턴스를 만들 수는 없다.
// 추상 메소드는 추상 클래스 안에서 만들 수 있는데,
// 메소드(함수)의 실행 부분은 클래스 안에서 구현하지 않고 해당 메소드의 call signature를 만든다.
// 추상 메소드는 추상 클래스를 상속받는 모든 것들이 구현해야하는 메소드이다.
abstract class User {
  constructor(public firstName: string, protected lastName: string, protected nickname: string) {}

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  private getLastName() {
    return this.lastName;
  }

  // 추상 메소드 call signature 생성
  abstract getNickName(): void;
}

class ClassPlayer extends User {
  // 상속 클래스 내부에서 추상 메소드 구현
  getNickName() {
    console.log(this.nickname);
  }
}
const jiho = new ClassPlayer('jiho', 'shin', 'jiyaho');

jiho.getFullName(); // User로부터 상속받았으므로 getFullName 메소드 사용 가능
// jiho.getLastName(); // 메소드의 속성이 private이므로 사용 불가

// 클래스로 Hash Map 만들기
type Words = {
  [key: string]: string;
};

class Dict {
  private words: Words;
  constructor() {
    this.words = {}; // 초기화
  }
  add(word: Word) {
    // 클래스를 타입처럼 사용 가능
    if (this.words[word.term] === undefined) {
      this.words[word.term] = word.def;
    }
  }
  def(term: string) {
    return this.words[term];
  }
}

class Word {
  constructor(public term: string, public def: string) {}
}

const kimchi = new Word('kimchi', '음식');

const dict = new Dict();
dict.add(kimchi);
dict.def('kimchi');

Interface

// type 정의는 꼭 concrete type(String,Number,Boolean 등)이 아니어도 가능
type Color = 'red' | 'blue' | 'yellow';
type Menu = {
  name: string;
  color: Color;
};
const board: Menu = {
  name: 'board',
  color: 'red',
};

// Object의 모양을 알려주는 방법은 위와 같은 type생성과 아래와 같은 interface생성으로도 가능
interface Menu1 {
  name: string;
  color: Color;
}

// type과 interface의 차이?
// interface는 오브젝트 외에 변수 등의 타입을 정해줄 수 없음. 오직 오브젝트만 가능.
// 통상적으로 오브젝트나 클래스에는 인터페이스, 나머지 부분에는 타입을 이용함.

// 타입처럼 명시하는 것도 가능
function makeMenu(menu: Menu1): Menu1 {
  return {
    name: 'board',
    color: 'yellow',
  };
}

// 클래스 처럼 확장해서 사용가능
interface Animal {
  name: string;
}
interface Bear extends Animal {}
const teddy: Bear = {
  name: 'teddy',
};

// 위와 같은 확장은 type으로도 가능
type User2 = {
  name: string;
};
type Player2 = User2 & {};
const joo2: Player = {
  name: 'joo',
};

// 인터페이스는 같은 이름으로 여러 개 선언 가능.
// 여러 개로 나뉘어도 각각의 속성들 인식 가능(type의 경우엔 불가능)
interface User1 {
  name: string;
  age: number;
}
interface User1 {
  sex: string;
}
const kai: User1 = {
  name: 'kai',
  age: 20,
  sex: 'M',
};

// 타입스크립트에서의 추상 클래스는 JS로 변환 시, 일반 클래스로 변환 된다.
// 대신에 interface를 사용하여 클래스와 결합한 형태로 사용이 가능한데,
// interface는 JS에서 대체할 코드가 없어 컴파일되지 않으며 그로인해 JS코드는 더욱 가벼워진다.

interface NewUser {
  firstName: string;
  lastName: string;
  sayHi(name: string): string;
  fullName(): string;
}
interface UserInfo {
  address: string;
}
// 인터페이스 상속 시, 접근제어자 속성으로 private나 protected는 사용할 수 없음(public만 가능)
// 클래스 상속: class Player extends User {}
// 인터페이스 상속: class Player implements User {}
// cf. 타입도 상속 가능 (implements 이용)
class NewPlayer implements NewUser, UserInfo {
  constructor(public firstName: string, public lastName: string, public address: string) {}
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  sayHi(name: string) {
    return `Hello ${name}, My name is ${this.fullName}`;
  }
}

Interface, Class, Generic 적용 예제

interface SStorage<T> {
  [key: string]: T;
}

class LocalStorage<T> {
  private storage: SStorage<T> = {};
  
  // Create
  set(key: string, value: T) {
    if (this.storage[key] !== undefined) {
      return console.log(`${key}가 이미 존재합니다.`);
    }
    this.storage[key] = value;
  }
  // Read
  get(key: string): T | void {
    if (this.storage[key] === undefined) {
      return console.log(`${key}가 존재하지 않습니다.`);
    }
    return this.storage[key];
  }
  // Delete
  remove(key: string) {
    delete this.storage[key];
  }
  clear() {
    this.storage = {};
  }
}

// 제네릭 타입 지정
const stringsStorage = new LocalStorage<string>();
stringsStorage.get('keyName');
stringsStorage.set('nickname', 'jiyaho');

const booleanStorage = new LocalStorage<boolean>();
booleanStorage.get('xxx');
booleanStorage.set('xxx', true);

Reference.

profile
😸

0개의 댓글