TypeScript

jeongjwon·2023년 5월 31일
0

SEB FE

목록 보기
53/56

열거형 Enum

JavaScript 에서 열거형 대신 객체 형태로 나타내지만, TypeScript 에서는 문자형 열거형과 숫자형 열거형을 지원하며, 특정 값(상수)의 집합을 정의할 때 사용된다.

숫자형 열거형

(auto-incrementing) : 값을 지정하지 않는다면 자동적으로 0부터 시작하여 1씩 증가하게 값이 지정된다.

enum Color{
  Red, //0
  Green, //1
  Blue. //2
}
let c: Color; //enum 타입 변수 선언
c = Color.Green; //enum 타입 변수에 값 할당
c = 'Hello'; //Error : enum 타입변수로 지정했기 때문에 설정된 enum 값인 Red, Green, Blue 만 올수 없음

let d: Color.Red; //enum 값을 타입 자체로도 사용할 수가 있다.
  • 이처럼 수동으로 값을 명시할 수 있고, 값을 변경한 부분부터 다시 1씩 증가하게 값이 매겨진다.
enum Week {
  Sun, //0
  Mon = 22,
  Tue, //23
  Wed, //24
  Thu, //25
  Fri, //26
  Sat //27
}
console.log(Week.Mon); // 22
console.log(Week.Tue); // 23
  • (역매핑 Reverse mappings) : 역방향으로 매겨진 값 value 으로 enum의 키 key 를 알아 낼 수 있다. 숫자형 Enum 에서만 가능하다.
enum Week {
  Sun,
  Mon,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat
}

console.log(Week.Sun); // 0
console.log(Week['Sun']); // 0
console.log(Week[0]); // 'Sun'

let weekName: string = Week[0];
console.log(weekName); // 값이 0인 일요일이 출력 -> Sun

문자열 열거형

  • 각각에 키에 문자열 값으로 할당되어 있다.
  • 숫자열 enum 과 다르게 auto-incrementing 기능이 없지만, 항상 명확한 값으로 디버깅하기가 수월하다.
  • 아쉬운 점은 문자열 열거형에서는 역매핑을 사용할 수 없다.
enum Color {
   Red = 'red',
   Green = 'green',
   Blue = 'blue',
}

console.log(Color.Red); // red
console.log(Color['Green']); // green


인터페이스 Interface

자바에서 클래스 위주로 인터페이스를 다루지만, 타입스크립트에서는 객체를 위주로 다룬다. 인터페이스는 객체의 껍데기로써 변수, 함수, 클래스에 사용 가능하다.

1. interface 예약어를 사용하여 인터페이스를 생성한다.

2. 인터페이스의 이름은 꼭 대문자로 시작한다.

//예약어 interface + 대문자 네이밍 컨벤션 으로 인터페이스 생성
interface Human{
  name: string;
  age: number;
  boo(): void;
}

3. 생성된 객체는 인터페이스 내에 선언된 프로퍼티와 메서드에 맞게 작성해야한다.

  • 프로퍼티 개수나 타입에 맞게 선언해야한다.
//인터페이스 자체를 타입으로 줘서 객체 생성
const person: Human = {
  name: 'Kane',
  age: 30,
  boo: () => conso.log('He is in M.U.');
};

4. 선택적 프로퍼티도 작성 가능하다.

  • 콜론: 앞에 ? 를 붙이면 옵션 속성이 된다.
  • 초기 변수를 설정할 때 명시하지 않아도 객체는 생성이 되며, 나중에 선택적 프로퍼티를 추가적으로 선언할 수 있다. 이는 사실 Union 타입으로 지정한 타입 | undefined 선언이나 마찬가지이다.
interface CraftBeer{
  name: string;
  hope?: number; //hope 속성은 명시해도 되고 안해도 된다.
}
function brewBeer(beer: CraftBeer){
  console.log(beer.name);
}
let myBeer = { name: 'Saporo' }; //hope 속성 없이 name 속성만 설정
brewBeer(myBeer); //Saporo

myBeer.hope = 5; // 선택적 프로퍼티에 의해서 나중에 속성값을 넣어줄 수 있다.

5. 함수타입을 정의할 때에도 사용할 수 있다.

  • 함수의 매개변수 타입과 반환타입만으로 함수를 정의할 수 있다.
  • 이미 인페이스에서 매개변수의 타입과 반환타입이 작성되어 있기 때문에 타입 추론 기능에 의해 생성된 함수에서는 굳이 타입을 명시하지 않아도 된다.
//매개변수에서 인터페이스를 타입으로 받을 수 있다.
function booboo(a: Human): void{
  console.log(`${a.name} is ${a.age} years old`);
};

booboo(person); // Kane is 30 years old
person.boo(); // He is in M.U.




interface Login {
  //함수명이 아닌 함수의 모양(인자, 리턴) 타입을 쓴다.
  (name: string, password: string) : boolean;
}

let loginUser: Login = function(id, pw) {
  //타입추론에 의해 선언할 함수에 타입을 굳이 쓸 필요가 없다.
  console.log('로그인 했습니다.');
  return true;
}
console.log(loginUser('jw', '123'));//로그인 했습니다. true
loginUser('jw', '123'); //로그인 했습니다.

6. 클래스에서도 사용 가능하다.

  • implements 키워드를 사용해 클래스 정의 옆에 붙여주면 된다.
  • 인터페이스에서 정의된 함수나 메서드의 매개변수 타입과 반환값이 일치하도록 구현해야하고, 타입도 명시해주는 것이 좋다.
interface IUser{
  name: string;
  getName(): string;
}

class User implements IUser{
  name: string;
  constructor(name: string){
    this.name = name;
  }
  getName(){
		return this.name;
  }
}

const neo = new User('Neo');
neo.getName(); // Neo

7. 확장성

  • extends 키워드를 사용한다.
  • 여러 인터페이스를 확장할 수 있다.
  • as 를 이용해 객체를 생성할 수 있다.
interface Person {
  name: string;
  age: number;
}
interface Devloper extends Person{
  language: string;
}
const person: Developer = {
  name: 'John',
  age: 25,
  language: 'JAVA',
}


interface Programmer{
  favoriteProgrammingLanguage: string;
}

interface Korean extends Person, Programmer{
  // 두 개의 인터페이스를 받아 확장
  isLiveInSeoul: boolean;
}
const person: Korean = {
  name: '홍길동',
  age: 33,
  favoriteProgrammingLanguage: 'kor',
  isLiveInSeoul: true,
}

// as 이용시 프로퍼티를 직접 명명해야 한다.
const person = {} as Korean;
person.name = '홍길동';
person.age = 33;
person.favoriteProgrammingLanguage: 'kor',
person.isLiveInSeoul: true,


타입 별칭 Type Aliases

타입의 새로운 이름을 만드는 것으로 기존의 타입을 참조하는 것을 의미한다.

1. type 키워드를 사용한다.

2. 확장성 x

  • 타입에 새로운 이름을 부여하는 것이므로 확장이 되지 않는다.
  • 다만, 인터페이스는 인터페이스, 타입 둘다 상속가능하기 때문에 인터페이스를 쓰는 것이 효율적이다.
type MyString = string;
let str1: string = 'hello';
let str2: MyString = 'Hello world';
//string 타입으로 사용하고 MyString === string 동일한 의미를 갖는다.

// 리터럴 객체 타입
const a1: {
  name: string;
  age: number;
  talk: () => void;
} = {
  name: '홍길동',
  age: 12,
  talk(){},
};

//type alias 객체 타입
type Ty = {
  name: string;
  age: number;
  talk: () => void;
};
const a2: Ty = {
  name: '홍길동',
  age: 12,
  talk(){},
};

// 인터페이스 객체 타입
interface In {
   name: string;
   age: number;
   talk: () => void;
}
const a3: In = {
   name: '홍길동',
   age: 12,
   talk() {},
};
type Person = {
  name: string;
  age: number;
}

interface User {
  name: string;
  age: number;
}

//type -> type 확장 불가
type Students extends Person {
	classNmae: string;
}

//interface -> interface 확장 가능
interface Students extends User{
  className: string;
}

//type -> interface 확장 가능
interface Students extends Person{
  className: string;
}




타입 추론 Type Inference

타입스크립트이 정적타입을 지원하는 언어라 타입추론 기능 특징을 가진다.
타입 추론이란 개발자가 굳이 변수 선언할 때 타입을 쓰지 않아도 컴파일이 스스로 판단해서 타입을 넣어주는 것을 말한다.

장점

  1. 코드의 가독성 향상
  2. 개발 생산성 향상
  3. 오류 발견 용이성

단점

  1. 타입 추론이 잘못될 경우 코드 오류 발생
  2. 명시적 타입 지정이 필요한 경우가 있음



클래스 Class

자바스크립트와 더불어 타입스크립트는 객체 지향 프로그래밍이므로 클래스를 사용할 수 있다.

1. 클래스 바디에 생성자 메서드와 함께 인스턴스 변수를 정의해주어야 한다.

  • 인스터스나 생성자 메서드에서도 타입을 명시해줘야 한다.
class Animal{
  name: string;
  contructor(name: string){
    this.name = name;
  }
}

2. 확장성

  • extends 키워드를 사용하여 상속받아 확장할 수 있다.
    • 수퍼 클래스 Super Class : 상속 해주는 상위 클래스, 서브 클래스 Sub Class : 상속 받는 하위 클래스
    • 하나의 부모 클래스는 여러 개의 자식 클래스를 가질 수 있다.
    • 하나의 클래스는 여러 개의 클래스로부터 상속을 받을 수 없다.
    • 부모 클래스로부터 상속받은 자식 클래스는 부모의 클래스의 자원 모두를 사용 할 수 있고, override 하여 수정해서 사용 할 수 있다.
    • 부모클래스는 자식 클래스의 자원을 가져다 쓸 수 없다. (IS-A 관계)
class Cat extends Animal{
  //수퍼 클래스 Animal 을 상속함으로써 
  //부모 클래스의 인스턴스인 name 과 생성자 메서드는 작성하지 않아도 접근하여 사용가능하다.
  //추가 메서드를 작성시 부모 클래스의 인스턴스를 가져올 수 있다.
  getName(): string{
    return `Cat name is ${this.name}.`;
  }
}

let cat: Cat = new Cat('Lucy');
console.log(cat.getName()); //Cat name is Lucy.
IS-A
class Animal {
   name: string;

   constructor(name: string) {
      this.name = name;
   }

   move(distanceInMeters: number = 0) {
      console.log(`${this.name} moved ${distanceInMeters}m.`);
   }
}

class Snake extends Animal {
   leg: number = 0;
   
   // @Override Animal
   constructor(name: string) {
      super(name);
   }

   // @Override Animal
   move(distanceInMeters = 5) { // 오버라이드 되서 굳이 또 매개변수 타입을 선언 할 필요 없다.
      super.move(distanceInMeters);
   }

   poison() {
      console.log('shoot poison !!');
   }
}

class Horse extends Animal {
   leg: number = 4;

   // @Override Animal
   constructor(name: string) {
      super(name);
   }

   run() {
      console.log('start Run !!');
   }
}

Animal 부모 클래스를 extends 키워드를 사용하여 자식 클래스 Snake, Horse 가 상속받았다. 상속된 클래스의 생성자 함수는 부모 클래스의 생성자를 실행하기 위해 super() 를 호출해야한다.

자식 클래스 Snake 에서 move() 메서드를 Override 하였다. 이는 기존의 메서드를 덮어씌우고 기존 함수를 개조하는 개념이다.

let sam: Snake = new Snake('Sammy the Python'); // 자식 클래스 생성자로 객체 생성
sam.leg; // 자식 클래스의 인스턴스 변수 : 0
sam.move(); // 오버라이드 한 부모클래스의 메소드 : 'Sammy the Python moved 5m' (5만 변경)
sam.poison(); // 자식 클래스의 메소드 : 'shoot poison !!'

let tom2: Horse = new Horse('Tommy the Palomino');
tom2.leg; // 자식 클래스의 인스턴스 변수 : 4
tom2.move(34); // 오버라이드 한 부모클래스의 메소드 : 'Tommy the Palomino moved 34m'
tom2.run(); // 자식 클래스의 메소드 : 'start Run !!'
let tom: Animal = new Horse('Tommy the Palomino');
tom.leg; //Animal 형식에 leg 속성이 없습니다.
tom.move(34);
tom.run(); //Animal 형식에 run 속성이 없습니다.

tom 변수의 타입을 Animal로 지정하고 자식 클래스인 Horse 생성자를 받았다.
부모 클래스로 타입형을 선언하고 자식클래스를 생성해서 할당하였기 때문에, 부모 클래스의 인스턴스인 name과 move()만 사용할 수 있고, Horse의 인스턴스인 leg와 run() 를 사용할 수 없어 이런 에러가 발생하였다.

3. 클래스 접근 제어자

접근 제어자는 클랫, 메서드 및 기타 멤버의 접근 가능성을 설정하는 객체 지향 언어의 키워드이다.

  • pulic : 어디서나 자유롭게 접근 가능하고 기본값으로 생략가능하다.
class Animal {
   public name: string;
   public constructor(theName: string) {
      this.name = theName;
   }
   public move(distanceInMeters: number) {
      console.log(`${this.name} moved ${distanceInMeters}m.`);
   }
}

// 둘이 완전히 같은 구조이다.

class Animal {
   name: string;
   constructor(theName: string) {
      this.name = theName;
   }
   move(distanceInMeters: number) {
      console.log(`${this.name} moved ${distanceInMeters}m.`);
   }
}
  • private : 내 클래스에서만 접근 가능
class Animal {
   // private 수식어 사용
   private name: string;
   constructor(name: string) {
      this.name = name;
   }
}

class Cat extends Animal {
   getName(): string {
      return `Cat name is ${this.name}.`; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'
   }
}

let cat = new Cat('Lucy');
console.log(cat.getName());
console.log(cat.name); // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.

cat.name = 'Tiger'; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
console.log(cat.getName());

Animal 을 상속받은 서브 클래스 Cat 은 수퍼 클래스에서 private 으로 선언한 name 인스턴스에 접근하지 못하기 때문에 오류가 발생한다. name 은 오직 Animal 클래스만 사용가능하고 다른 클래스에서는 사용할 수 없다.

4. 읽기 전용 readonly

클래스 내 인스턴스는 오직 읽기 전용으로 명시하기 위해서는 readOnly 키워드를 사용한다. 변경해서 안될 프로퍼티 값을 클래스 내 선언 시에 작성해준다. 추후에는 그 인스턴스는 다른 값으로 할당될 수 없다.

class Animal {
   readonly name: string; // 읽기 전용 속성

   constructor(n: string) {
      this.name = n; // 읽기 전용은 초기화 할때만 값 대입 가능
   }
}

let dog = new Animal('Charlie');
console.log(dog.name); // Charlie

// 그러나 초기화가 아닌 추후에 접근해서 할당은 불가능
dog.name = 'Tiger'; // Error - TS2540: Cannot assign to 'name' because it is a read-only property.

제네릭 Generic

프로그래밍을 할 때 변수는 변할 수 있는 것, 상수는 항상 고정된 값을 말한다. number, string[] 과 같이 타입은 항상 고정되어 절대 변하지 않는 타입을 사용해오고 있다. 유연성을 위해 number | string | undefined 과 같이 | 를 사용해 union 타입을 사용하기도 했다.

하지만 항상 고정되어 의도대로 흘러가지는 않는다. 변수로 언제든지 변할 수 있는 타입(타입을 변수화)을 통해 보다 유연성있고 코드 재사용성을 높이고 안정성을 보장할 수 있는 바로 제네릭 기능이다.

  • 타입이 고정되는 것을 방지하고 재사용 가능한 요소를 선언할 수 있다.
  • 타입 검사를 컴파일 시간에 진행함으로써 타입 안저성을 보장한다.
  • 제네릭 로직을 이용해 타입을 다르게 받을 수 있는 재사용 코드를 만들 수 있다.
function add(x: string|number , y: string|number): string | number {
	return x + y;
}
add(1,2); //3
add('hello', 'world'); //helloworld

add(1,'2');
add('1', '2');

유니언을 통해 매개변수 x, y 는 string 혹은 number 의 타입을 모두 받을 수 있다. 하지만, x: string, y: number 혹은 x: number, y: string 도 가능하지만 컴파일러는 이를 에러로 감별한다.

우리는 이것을 각각 타입을 분리하여 오버로딩을 할 수 있다.

function add(x: string, y: string): string ;
function add(x: number, y: number): number;
function add(x: any, y: any){
  return x+y;
}

add(1,2); //3
add('hello', 'world'); //helloworld

add(1,'2'); // 호출 x
add('1', '2'); // 호출 x

밑의 두 함수는 전달인자가 아예 받아들여지지 않기 때문에 함수 호출이 일어날 수가 없다.
하지만 타입별로 구현해야하는 함수는 코드가 길어지게 되고 가독성이 떨어지는 단점이 있어 이런 한계를 극복하기 위해 제네릭이 사용된다.

//함수형
function add<T>(x:T, y:T): T{
  return x+y;
}
//화살표 함수형
const add2 = <T>(x:T, y:T): T => { ... }
add<number>(1,2); //3
add<string>('hello', 'world'); //helloworld

꺽쇠<> 기호를 변수명, 함수명 앞에 쓰면 타입 단언이 되게 된다.
변수명을 뜻하는 문자 T 를 이용하여 제네릭을 표현한다.
하지만 코드가 복잡해서 컴파일러가 간혹 멍청하게 타입 추론을 잘못한다면 직접 제네릭을 선언해야하는 경우가 있다.

배열과 제네릭

인수를 배열로 받을 경우는 제네릭 처리를 T[]Array<T> 해주어야 한다.

function loggingIdentity<T>(arg: T[]): T[]{
  console.log(arg.length);
  return arg;
}
function loggingIdentity2<T>(arg: Array<T>): Array<T>{
  console.log(arg.length);
  return arg;
}

인터페이스와 제네릭

//제네릭 인터페이스
interface Mobile<T>{
  name: string;
  price: number;
  option: T; // 제네릭 타입 
}

// 제네릭 자체에 리터럴 객테 타입도 할당 할 수 있다.
const m1: Mobile<{color: string; coupon: boolean}> = {
  name: 's21',
  price: 1000,
  option: { color: 'read', coupon: false},
  // 제네릭 타입의 의해서 option 속성이 유연하게 타입이 할당됨
};

const m2: Mobile<string> = {
   name: 's20',
   price: 900,
   option: 'good', // 제네릭 타입의 의해서 option 속성이 유연하게 타입이 할당됨
};

Mobile 인터페이스를 사용하여 만든 객체는 option의 값으로 어떤 타입이 들어갈지만 작성해주면 굳이 여러 개를 만들지 않고도 m1이나 m2 처럼 재사용할 수 있다.

type TG<T> = T[] | T;

const number_arr: TG<number> = [1,2,3,4,5];
const number_arr2: TG<number> = 12345;

const string_arr: TG<string> = ['1', '2', '3', '4', '5'];
const string_arr2: TG<string> = '12345';

type alias 과 제네릭을 같이 사용할 수 있다.

클래스와 제네릭

class GenericNumber<T> {
   zeroValue: T;
   add: (x: T, y: T) => T;

   constructor(v: T, cb: (x: T, y: T) => T) {
      this.zeroValue = v;
      this.add = cb;
   }
}

let myGenericNumber = new GenericNumber<number>(0, (x, y) => {
   return x + y;
});

let myGenericString = new GenericNumber<string>('0', (x, y) => {
   return x + y;
});

myGenericNumber.zeroValue; // 0
myGenericNumber.add(1, 2); // 3

myGenericString.zeroValue; // '0'
myGenericString.add('hello ', 'world'); // 'hello world'

클래스도 인터페이스처럼 생성자 함수로 타입을 참조해야한다.

제네릭 제약 조건

제네릭은 사용하는 시점에 타입을 결정해줌으로써 사실상 아무 타입이나 집어넣어도 상관없다.

function identity<T>(p1: T): T {
   return p1;
}

identity(1);
identity('a');
identity(true);
identity([]);
identity({});

입력값에 대한 유연성은 확보했지만, 각 함수에 대해 사용처에 따라 입력값을 제한 할 필요가 생긴다.
이를 위해 extends 키워드를 사용해 적용되는 타입의 종류를 제한할 수 있는 기능을 제공한다.
클래스의 extends 는 상속하여 확장의 의미를 가지지만, 제네릭에서의 extends 는 제한의 의미를 가진다.
<T extends K> T가 K에 할당 가능해야 한다 를 의미한다.

type numOrStr = number | string;

// 제네릭에 적용될 타입에 number | string 만 허용
function identity<T extends numOrStr>(p1: T): T {
   return p1;
}

identity(1);
identity('a');

identity(true); //! ERROR
identity([]); //! ERROR
identity({}); //! ERROR

T는 number | string 에만 할당 가능하기 때문에 boolean, [], {} 에는 허용되지 않는다.

속성 제약 조건

function loggingIdentity<T>(arg: T): T {
   console.log(arg.length);
   return arg;
}

T에는 .length 프로퍼티가 없어서 오류가 발생한다. 개발자 입장에서는 길이를 구하고자 하는 로직을 짜기 위해 length 를 사용하겠지만 컴파일러 입장에서는 타입을 알지 알지 못하기 때문에 충분히 에러가 발생한다.
그래서 타입 가드 를 통해 분기하는 방법도 있지만, 해당 속성을 포함하도록 지정해주어야 한다.

interface Lengthwise{
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  //제네릭 T는 반드시 {length: number} 프로퍼티 타입을 포함해야 한다.
  //arr에서 .length 프로퍼티가 존재함을 알기 때문에 접근가능하다.
   console.log(arg.length);
   return arg;
}

keyof 키워드를 통해 객체의 key 값만 뽑아 접근할 수 있다.


function getProperty<T, K extends keyof T>(obj: T, key: K) {
   return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, 'a'); // 성공
getProperty(x, 'm'); // 오류: 인수의 타입 'm' 은 'a' | 'b' | 'c' | 'd'에 해당되지 않음.





원래 자바언어를 썼어서 그런지 자바스크립트를 쓸 때 타입을 구분하지 않아서 생소했는데, 타입스크립트에서는 원래 알던 대로 타입을 지정할 수 있어서 큰 어려움은 없었다.
타입 추론 기능이나 제네릭 등은 아직 생소하여서 예제나 과제를 통해 더 많이 접해야 할 필요가 있는 것 같다.

0개의 댓글