[데브코스] 프론트엔드 엔지니어링 MIL(TypeScript)

홍건우·2023년 7월 26일
1

데브코스

목록 보기
13/17
post-thumbnail

TypeScript란?

  • 타입스크립트는 동적 타입의 자바스크립트에 정적 타입을 부여한 언어로 자바스크립트의 확장된 언어라고 볼 수 있음
    • JS: 동적 타입 ⇒ 런타임에서 동작할 때 타입 오류 확인
    • TS: 정적 타입 ⇒ 코드 작성 단계에서 타입 오류 확인
  • 타입스크립트로 작성된 파일은 실행전 자바스크립트 파일로 컴파일하는 과정이 필요함

    npm i typescript -D // 개발모드에 TS설치

    npx tsc --init // 타입스크립트의 컴파일 옵션을 기본으로 실행함

    npx tsc // 프로젝트 폴더에 안에 있는 모든 TS파일이 tsconfig에 맞춰서 JS로 컴파일됨

TypeScript에서 제공하는 데이터 타입

ECMAScript 표준에 따른 기본 자료형 6가지

  • Primitive Type
    • Boolean
    • Number
    • String
    • Null
    • Undefined
    • Symbol
  • Array: object형

추가적으로 제공하는 타입

  • Any, Void, Never, Unknown
  • Enum
  • Tuple: object 형(Array와 유사한 형태)

타입 설명

Symbol

  • primitive값을 담아서 사용하며 고유하고 수정불가능한 값으로 만들어 줌. (주로 접근을 제어하는데 쓰는 경우가 많음)
  • 함수로 사용할때는 대문자로 Symbol, 타입으로 사용할 때는 소문자로 symbol을 사용

null & undefined

  • tsconfig(컴파일 옵션)에 설정을 따로 하지 않는 이상 number에 null과 undefined를 할당할 수 있음.
  • 이를 막기위해 컴파일 옵션에서 ‘--strictNullChecks’를 사용하면 null과 undefined는 void(undefined만 가능)나 자기 자신들에게만 할당 가능 ⇒ null과 undefined를 할당할 수 있게 하려면, union type을 이용해야 함
let union: string | null = null;

object

  • primitive type이 아닌 것을 나타내고 싶을 때 사용하는 타입
  • non-primitive type: not number, string, boolean, bigint, symbol, null, undefined
//create by object literal
const person1 = {name: 'gu', age: 24};
//person1은 "object" type이 아님, person1은 "{name: string, age: number}" type

//create by Object.create
const person2 = Object.create({name: 'gu', age: 24});

Array

  • Array - 이 방식은 jsx, tsx에서 충돌이 발생할 가능성이 있기 때문에 사용하지 않는 것이 좋음
  • type[]
let list: Array<number> = [1, 2, 3];
let list: number[] = [1, 2, 3];

tuple

  • 배열의 길이가 고정되고 각 요소의 타입이 지정되어 있는 배열 형식을 의미
  • 즉 순서에 따른 타입이 정확하게 할당되어야 함.
  • push, splice 등을 통해 배열의 길이가 변경이 가능하므로 사용에 주의해야함
let x: [string, number];
x = ["hello", 24];
x = [10, "hello"]; //error
x[2] = 'go'; //error

any

  • 모든 타입에 대해 허용
  • 컴파일 옵션중에 any를 작성하지 않으면 오류를 표시하도록 하는 옵션이 있음 → noImplicitAny
  • any는 계속해서 개체를 통해 전파됨 → 타입 안전성은 TS를 사용하는 주요 동기 중 하나이기 때문에 필요하지 않는 경우에는 any를 사용하지 않도록 해야함
  • any가 개체를 통해 전파되는 예
    let looselyTyped: any = {};
    let d = looselyTyped.a.b.c.d;
    //위의 코드는 코드 작성 시 오류가 발생하지 않으며 d의 타입은 any로 추론됨.

unknown

  • any가 주는 타입의 불안전함을 어느정도 해소하기 위해 나온 대체제
  • 컴파일러가 타입을 추론할 수 있게끔 타입의 유형을 좁히거나 타입을 확정해주지 않으면 다른 곳에 할당할 수 없고 사용할 수 없음
  • 따라서 any대신 unknown을 사용하는 것이 좋음
declare const maybe: unknown;

const aNumber: number = maybe; //error

if (maybe === true) {
	const aBoolean: boolean =  maybe;
	const aString: string = maybe; //error
}

if(typeof maybe === 'string'){
	const aString:string = maybe;
	const aBoolean: boolean =  maybe; //error
}

unknown 타입은 위와 같이 타입가드를 통해서 타입을 한정시켜야지만 사용할 수 있음

never

  • never 타입은 모든 타입의 subtype이며, 모든 타입에 할당 할 수 있음, 하지만 never에는 그 어떤 것도 할당할 수 없음. (any도 never에게 할당할 수 없음)
  • 함수의 끝에 절대 도달하지 않는다는 의미 또는 정상적으로 종료되지 않는 함수의 반환 타입으로 사용함
  • never 사용 예시
    function error(message: string): never{
    	throw new Error(message);
    }
    
    function fail(){
    	return error('failed');
    }
    
    function infiniteLoop(): never{
    	while(true) {
    		
    	}
    }

void

함수의 반환 값이 없다고 명시적으로 작성하는 타입

function returnVoid(message: string): void {
	console.log(message);
	return;
}

returnVoid("no return");

enum

  • 특정 값(상수)들의 집합을 의미
  • 타입 + 값의 집합으로 역방향 매핑 가능
enum Animals { Cat, Dog, Lion }
console.log(Animals[0]); // Cat
console.log(Animals.Cat); // 0
  • enum의 인덱스를 사용자 편의로 변경하여 사용 가능
enum Animals { Cat = 1, Dog, Lion }
console.log(Animals[1]); // Cat
console.log(Animals[3]); // Lion
  • 문자를 할당하게 되면 역방향 매핑이 불가능함
enum Animals {
  Cat = 'meow',
  Dog = 1,
  Lion = 2,
}

console.log(Animals.Cat); // meow
console.log(Animals[0]); //undefined

union

  • 2개 이상의 타입이 허용되는 타입
let uni: string | number = 0;
uni = 'hi';
uni = 123;

intersection

  • 2개 이상의 타입이 병합된 타입
type UserA = {
  name: string;
  age: number;
};

type UserB = {
  isValid: boolean;
};

const userA: UserA = {
  name: 'a',
  age: 12,
  isValid: true, // error
};

const userB: UserB = {
  name: 'B', // error
  age: 11,
  isValid: true,
};

const userC: UserA & UserB = {
  name: 'C', age: 40, isValid: false
};

타입 추론(Inference)

  • TypeScript가 작성된 코드에서 타입을 알 수 있는 경우
    • 초기화된 변수
    • 기본값이 지정된 매개변수
    • 반환이 있는 함수
  • 타입 추론을 통해 모든 곳에 타입을 명시할 필요가 없음
let a = 'Hello'; // string이라는 별도의 타입을 명시할 필요가 없음
a = 123; // error

function join(a: string, b = '') { //b 매개변수에 타입을 명시할 필요가 없음
  return a + b; // 함수의 반환 타입도 작성하지 않아도 됨
}

const a: number = join('hello', 'world'); // error

타입 단언(Assertion)

  • 개발자가 typescript에게 타입을 단언해서 명시할 수 있음
  • as, ! (Non-null 단언 연산자)
// as 키워드를 통해 btn이 버튼 엘리멘트임을 단언함
const btn = document.querySelector('button') as HTMLButtonElement;
// (btn as HTMLButtonElement).classList.add('btn'); 나중에 단언도 가능

// ! 키워드를 통해 btn 변수가 null이나 undefined가 아님을 단언함
const btn = document.querySelector('button')!;
// btn!.classList.add('btn'); 나중에 단언도 가능
// as 키워드 사용 예
function toTwoDecimals(val: number | string, isNum: boolean) {
  if (isNum) {
    (val as number).toFixed(2);
  } else {
    (val as string).slice(0, 2);
  }
}

toTwoDecimals(3.141592, true);
toTwoDecimals('hi', false);
const json = '{"name": "gugu", age: 22}';
const user = JSON.parse(json);
console.log(user.email); // 에러가 발생하지 않음

// as 키워드 사용 예
const json = '{"name": "gugu", age: 22}';
const user = JSON.parse(json) as { name: string, age: number};
console.log(user.email); // error
let num: number;
console.log(num); // 초기화가 되지 않아 undefined이므로 오류 발생

let num!: number; // 할당 단언
console.log(num); // 초기화가 되어졌다고 typescript가 판단하기 때문에 오류가 발생하지 않음

타입 가드

  • 타입 추론이 가능한 특정 범위(scope) 안에서 타입을 보장하도록 하는 방법
  • typeof, instanceof, in 등을 통해 타입 가드가 가능
const btn = document.querySelector('button');
if (btn) { // if 조건을 통해 btn이 null이 아님이 보장됨
  btn.classList.add('btn');
  btn.id = 'abc';
}

if (btn instanceof HTMLButtonElement) { // 해당 방법도 가능
  btn.classList.add('btn');
  btn.id = 'abc';
}
function toTwoDecimals(val: number | string) {
  if (typeof val === 'number') { // typeof를 통해 val의 타입 가드를 형성함
    val.toFixed(2);
  } else {
    val.slice(0, 2);
  }
}

toTwoDecimals(3.14);
toTwoDecimals('hi');
type UserA = { name: string; age: number };
type UserB = { id: string; email: string };

// is 키워드를 통해 isUserA라는 함수가 UserA 인것을 확인하는 용도의 함수라는 것을 알림
function isUserA(user: unknown): user is UserA {
  if (user && user.constructor === Object) {
    const u = user as UserA;
    return typeof u.name === 'string' && typeof u.age === 'number';
  }
  return false;
}

fetch('https://test.com')
  .then((res) => res.json())
  .then((user: UserA | UserB) => {
    if (isUserA(user)) {
      console.log(user.name[0]);
      console.log(user.age - 10);
    }
  });

타입 별칭(Type Alias)

  • Interface와 비슷하게 보임
    • Interface: 어떤 타입이 타입으로서 목적, 존재 가치가 명확하면 사용
    • Type Alias: 다른 대상을 가리키거나 별명으로서만 존재하면 사용
  • Primitive, Union Type, Tuple, Function, 기타 직접 작성해야하는 타입을 다른 이름을 지정할 수 있음
  • 만들어진 타입의 refer로 사용하는 것이지 타입을 직접만드는 것이 아님
  • type은 파스칼 케이스를 주로 사용함

Aliasing Primitive

type MyStringType = string;

const str = 'world';

let myStr: MyStringType = 'hello';
myStr = str;

Aliasing Union Type

type StringOrNumber = string | number;

let another: StringOrNumber = 0;
another = 'Hi';

Aliasing Tuple


type PersonTuple = [string, number];

let another: PersonTuple = ['gu', 24];

Aliasing Function

type EatType = (food: string) => void;

Interface

개체(객체, 배열, 함수, 클래스 등)를 정의하는 타입

  • 객체의 스펙(속성과 속성의 타입)
  • 함수의 파라미터
  • 함수의 스펙(파라미터, 반환 타입 등)
  • 배열과 객체를 접근하는 방식
  • 클래스

optional property

interface 인터페이스_이름 {
  속성?: 타입;
}

위의 코드와 같이 속성뒤에 ?를 붙이게 되면 해당 속성은 옵션으로 변하게 됨 ⇒ type에서도 사용이 가능함

인덱스 시그니처(Index signature)

인덱싱 가능한 타입을 만들 때 인터페이스의 인덱스 시그니처 방식을 이용함

interface Arr {
  [key: number]: string;
}
const arr: Arr = ['A', 'B', 'C'];
console.log(arr[1]); // B

interface ArrLike {
  [key: number]: string;
}
const arrLike: ArrLike = { 0: 'A', 1: 'B', 2: 'C' };
console.log(arrLike[1]); // B

interface Obj {
  [key: string]: string;
}
const obj: Obj = { a: 'A', b: 'B', c: 'C' };
console.log(obj['b']); // B
console.log(obj.b); // B
  • 인터페이스에 정의하지 않은 속성들을 추가로 사용하고 싶을 경우는 아래와 같은 방법으로 사용할 수 있음
interface Person3 {
  name: string;
  age: number;
  [index: string]: string | number;
}

const p32: Person3 = {
  name: 'gugu',
  email: 'test@test.com',
  person_id: 1,
  age: 23,
};

타입 인덱싱

인터페이스라는 하나의 타입 안에 들어있는 속성의 또 다른 타입을 타입 인덱싱을 통해 얻어낼 수 있음

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

const a: User['name'] = 'gugu';
const b: User['age'] = 123;

function in interface

interface Person4 {
	name: string;
	age: number;
	hello(): void;
}

const p41: Person4 = {
	name: 'gugu',
	age: 12,
	hello: function(): void {
		console.log(`hi ${this.name}`);
	}
};

const p42: Person4 = {
	name: 'ququ',
	age: 11,
	hello(): void{
		console.log(`hi ${this.name}`);
	}
}

// arrow function에서는 this를 사용할 수 없기 때문에 오류 발생
// const p43: Person4 = {
// 	name: 'dudu',
// 	age: 11,
// 	hello: (): void => {
// 		console.log(`hi ${this.name}`);
// 	}
// } 

p41.hello();
p42.hello();

class implements interface

인터페이스를 이용해서 클래스(생성(구문) 시그니처(Construct signature))를 만들어내는 방법

interface UserI {
  name: string;
  getName(): string;
}

interface UserC {
  new (n: string): UserI; // 생성 시그니처
}

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

const user = new User('Gugu');
user.getName(); // Gugu

// userClass에는 생성 시그니처 타입이 정의되어야 함
function hello(userClass: UserC, msg: string) {
  const user = new userClass('Gugu');
  return `hello ${user.getName()} ${msg}`;
}

hello(User, 'hi');

interface extends interface

인터페이스를 확장하는데 사용함

interface IPerson2{
	name: string;
	age?: number;
}

interface IKorean extends IPerson2{
	city: string;
}

const k: IKorean = {
	name: 'gugu',
	city: 'seoul',
	age: 12,
};
interface User {
  name: string;
  age: number;
}

interface User { // 첫 부분의 User와 합쳐짐
  isValid: boolean;
}

const user: User = {
  name: 'Gu',
  age: 24,
  isValid: true
}
interface FullName {
  first: string;
  last: string;
}

interface FullName {
  mid: string;
  last: boolean // error
  // 중복해서 인터페이스를 정의할 경우 기존에 존재하는 속성에 다른 타입을 지정할 수 없음
}

function interface

interface HelloPerson {
	(name: string, age?: number): void;
}

const helloPerson: HelloPerson = function(name: string, age?: number) {
	console.log(`hi ${name}`)
}

Readonly Interface Properties

  • readonly: 읽기 전용 속성으로 설정하는 키워드 ⇒ type에서도 사용 가능
interface Person8 {
	name: string;
	age?: number;
	readonly gender: string;
}

const p81: Person8 = {
	name: 'gugu',
	gender: 'male',
}

p81.gender = 'female'; //읽기 전용 속성이므로 'gender'에 할당할 수 없음

함수의 명시적 this 타입

interface User {
  name: string
}

// this의 타입을 명시적으로 정의할 수 있음
function greet(this: User, msg: string) {
  return `${msg}, ${this.name}`;
}

const gu = {
  name: 'gu',
  greet
};

gu.greet('Hi'); // Hi, gu

const neo = {
  name: 'Neo',
};

greet.call(neo, 'Bye');

type alias vs interface

type alias: 어떤 타입을 부르는 이름을 말함

interface: 새로운 타입을 만들어 내는 것 ⇒ 객체 타입을 정의할 땐 interface를 주로 사용함

  • function에 대한 차이
    //type alias
    type EatType = (food: string) => void;
    
    //interface
    interface IEat {
    	(food: string): void;
    }
  • array
    //type alias
    type PersonList = string[];
    
    //interface
    interface IPersonList {
    	[index: number]: string;
    }
  • intersection
    interface ErrorHandling {
      success: boolean;
      error?: { message: string };
    }
    
    interface ArtistsData {
      artists: { name: string }[];
    }
    
    // type alias
    type ArtistsResponseType = ArtistsData & ErrorHandling;
    
    // interface
    interface IArtistsResponse extends ArtistsData, ErrorHandling {}
    
    let art: ArtistsResponseType;
    let iar: IArtistsResponse;
  • union types
    interface Bird {
      fly(): void;
      layEggs(): void;
    }
    
    interface Fish {
      swim(): void;
      layEggs(): void;
    }
    
    type PetType = Bird | Fish;
    
    interface IPet extends PetType {} // error
    
    class Pet implements PetType {} // error

Declaration Merging

  • interface
    interface Person {
      name: string;
    }
    
    interface Person {
      age: number;
    }
    
    let person: Person = {
      name: 'gugu',
      age: 12
    }
    
    console.log(person)
  • type alias는 할 수 없음.

타입스크립트에서 함수 사용

const foo1: (a: string, b: number) => string = (a, b) => {
  return a;
};

const foo2 = (a: string, b: number): string => {
  return a;
};

const foo3: (a: string, b: number) => string = function (a, b) {
  return a;
};

const foo4 = function (a: string, b: number): string {
  return a;
}

function foo5(a: string, b: number): string {
  return a;
}

함수 오버로딩

function addString(x: string, y: string): string {
  console.log(x, y);
  return x + y;
}

function addNumber(x: number, y: number): number {
  console.log(x, y);
  return x + y;
}

function add(x: string, y: string): string; // 구현부
function add(x: number, y: number): number; // 구현부
function add(x: any, y: any): any { // 선언부
  console.log(x, y);
  return x + y;
}

add('Hi', 'World');
add(12, 13);
add('Hi', 13); // error
add(12, 'World'); // error
interface UserBase {
  name: string;
  age: number;
}

interface User extends UserBase {
  updateInfo(newUser: UserBase): User;
  updateInfo(name: string, age: number): User;
}

const user: User = {
  name: 'gu',
  age: 24,
  updateInfo: function (nameOrUser: UserBase | string, age?: number) {
    if (typeof nameOrUser === 'string' && age !== undefined) {
      this.name = nameOrUser;
      this.age = age;
    } else if (typeof nameOrUser === 'object') {
      this.name = nameOrUser.name;
      this.age = nameOrUser.age;
    }
    return this;
  },
};

console.log(user.updateInfo({ name: 'gu', age: 23 })); // { name: 'gu', age: 23, updateInfo: f }
console.log(user.updateInfo('Leon', 51)); // { name: 'Leon', age: 51, updateInfo: f }

Access Modifiers(접근 제어자)

  • 접근 제어자에는 public, private, protected가 있음
  • 클래스 내부의 모든 곳(생성자, 프로퍼티, 메서드)에 설정 가능
  • JS에서는 private를 지원하지 않아 프로퍼티나 메서드 이름 앞에 _를 붙여서 표현했음

Initialization in constructor parameters

class PersonClass {
	public constructor(public name: string, public age: number) {}
	
//위의 코드는 아래의 코드와 동일한 결과를 보여줌
	public name: string;
	public age: number;
	public constructor(name: string, age: number){
		this.name = name;
		this.age = age;
	}
}

const p4 = new PersonClass('Gu', 13);
console.log(p4);

readonly properties

  • 접근 제어자와 상관 없이 readonly 키워드가 있는 변수는 초기화 영역에서만 값 설정이 가능하고 나머지 모든 부분(class의 method도 포함)에서는 변경이 불가능함

Index Signatures in class

  • 프로퍼티 이름이 동적으로 들어오는 경우에 사용할 수 있다.
// { gu: 'male', mark: 'male, jade: 'male'}
// { gu: 'male', chloe: 'female', alex: 'male', anna: 'female'}

class Students {
	[index: string]: "male" | "female";
	gu: 'male' = 'male';
}

const a = new Students();
a.mark = 'male';
a.jade = 'male';
console.log(a);

const b = new Students();
b.chloe = 'female';
b.alex = 'male';
b.anna = 'female';
console.log(b);

Static Properties & Methods

class PersonClass {
	private static CITY = 'SEOUL';
	public static hello() {
		console.log('hi', PersonClass.CITY);
	}
}

const p = new PersonClass();
// p.hello();

PersonClass.hello(); //hi SEOUL
  • 사용 예시
    class PersonClass {
    	private static CITY = 'SEOUL';
    	public hello() {
    		console.log('hi', PersonClass.CITY);
    	}
    	public change() {
    		PersonClass.CITY = 'LA';
    	}
    }
    
    const p = new PersonClass();
    p.hello(); // hi SEOUL
    const p12 = new PersonClass();
    p12.hello(); // hi SEOUL
    p.change();
    p12.hello();  // hi LA

Singletons(싱글톤 패턴)

  • sigeletons: 애플리케이션이 실행되는 중간에 클래스로부터 단 하나의 오브젝트만 생성해서 사용하는 패턴을 말함
  • 사용법
    • private constructor를 사용해 new 키워드로 클래스에 대한 오브젝트 생성을 막는다.

    • 오브젝트를 처음 생성하거나 생성된 오브젝트가 이미 존재한다면 그걸 리턴하는 메서드를 만든다.

      class ClassName {
      	private static instance: ClassName | null = null;
      	public static getInstance(): ClassName {
      		// ClassName으로부터 만든 object가 있으면 그걸 리턴
      		// 없으면, 만들어서 리턴
      		if (ClassName.instance === null) {
      			ClassName.instance =  new ClassName();
      		}
      		return ClassName.instance;
      	}
      
      	private constructor() {}
      }
      
      const a = ClassName.getInstance();
      const b = ClassName.getInstance();
      
      console.log(a === b); // true

Abstract Classes

  • 완전하지 않은 class를 표현하며 new를 이용해서 객체를 생성할 수 없음
  • abstract class를 상속받으면 abstract function을 구현해야함
    abstract class AbstractPerson {
      abstract name: string;
      abstract getName(): string;
    }
    
    // new AbstractPerson(); 에러 발생
    
    class PersonClass extends AbstractPerson {
      constructor(public name: string) {
        super();
      }
      getName(): string {
        return this.name;
      }
    }
    
    const p = new PersonClass('GuGuGu');
    console.log(p.getName());
  • interface의 implements와 abstract의 extends를 동시에 사용할 수 있음(extends 키워드가 implements보다 앞으로 와야함)
    class Cat extends CatA implements AnimalI1, AnimalI2 {
    	...
    }

interface와의 차이점

  • interface를 implements할 때는 super가 필요없지만 abstract를 extends할 때는 super가 필요함
  • type만을 선언하는 interface와는 다르게 abstract는 type 선언과 구현부 코드를 포함할 수 있음
    abstract class AnimalA {
      abstract color: string;
      abstract getColor(): string;
      constructor(public sound: string) {}
      speak() {
        return this.sound;
      }
    }
    
    class Dog extends AnimalA {
      constructor(
        sound: string,
        public color: string,
      ) {
        super(sound);
      }
      getColor() {
        return this.color;
      }
    }
  • 추상 클래스는 단일로(Is-A 관계)만 사용할 수 있지만 interface는 여러개(Has-A)를 사용할 수 있음
    class Cat implements Animal1, Animal2 {
    	...
    }

데코레이터

  • 클래스에서 사용하는 개념
  • 하나의 함수를 클래스, 클래스의 속성, 클래스의 메소드, 메소드의 매개 변수에 연결할 수 있음
    function deco(target: any) {
      console.log(target);
      return class {
        constructor(
          public a: any,
          public b: any,
        ) {
          console.log(this.a, this.b);
        }
      } as any;
    }
    
    @deco // 해당 부분에서 User클래스는 자신의 클래스 구조가 아닌
    // deco 함수가 반환한 클래스의 구조를 가지게됨
    class User {
      constructor(public name: string) {}
    
      hello(msg: string) {
        return `Hello ${this.name}, ${msg}`;
      }
    }
    
    const gu = new User('Gu', 21); // deco 함수의 반환값인 클래스로 객체 생성
    const neo = new User('Neo', 22); // deco 함수의 반환값인 클래스로 객체 생성
    console.log(gu);
    console.log(neo);
  • 예: 클래스에서 객체를 생성할 때마다 서버에 데이터를 보내는 코드 작성
    ⇒ 생성자 함수를 호출할때마다 데코레이터 함수의 반환문이 실행된됨 즉 데코레이터의 반환문 이외의 코드는 클래스에 연결될때 한 번만 실행됨.
       function deco<T extends { new (...args: any[]): any }>(target: T) {
         if (target.name !== 'User') {
           throw new Error('클래스의 이름이 User가 아님');
         }
         console.log('정상적인 클래스의 이름');
       
         // 반환하는 클래스의 연결된 클래스를 확장해서 복사함
         return class extends target {
           constructor(...args: any[]) {
             super(args);
             fetch('서버 주소', {
               //
             });
             console.log('서버로 데이터를 보냄');
           }
         };
       }
       
       @deco // 해당 부분에서 User클래스는 자신의 클래스 구조가 아닌
       // deco 함수가 반환한 클래스의 구조를 가지게됨
       class User {
         constructor(public name: string) {}
       
         hello(msg: string) {
           return `Hello ${this.name}, ${msg}`;
         }
       }
       
       const gu = new User('Gu'); // deco 함수의 반환값인 클래스로 객체 생성
       const neo = new User('Neo'); // deco 함수의 반환값인 클래스로 객체 생성
       console.log(gu);
       console.log(neo);

제네릭(Generics)

Generic과 Any의 차이점

Any는 input에 따라 달라지는 연산이 불가능하지만 Generic은 가능함

function hello(message: any): any {
	return message;
}

console.log(hello('mark').length);
console.log(hello(13).length); //오류가 발생하지 않음

function helloGenric<T>(message: T): T {
	return message;
}

console.log(helloGenric('mark').length);
console.log(helloGenric(13).length); //오류 발생

Generic basic

function helloBasic<T, U>(message: T, commet: U): T {
	return message;
}

helloBasic<string, number>("dd", 12); //way 1 타입 직접 지정
helloBasic(23, 21); //way 2 타입 추론

Generic Interface

interface ToObj<T> {
  a: T;
  b: T;
}

function toObj<T extends string | number>(a: T, b: T): ToObj<T> {
  return { a, b };
}

toObj<string>('A', 'B');
toObj<number>(1, 2);
toObj<boolean>(true, false); // 에러 발생

Generic Array & Tuple

function helloArray<T>(message: T[]): T {
	return message[0];
}

helloArray(['hello', 'world']);
helloArray(["hello", 4]); //string|number union타입으로 추론

function helloTuple<T, K>(message: [T, K]): T {
		return message[0];
}

helloTuple(["hello", "world"]);
helloTuple(["hello", 1]);

Generics Function

type HelloFunctionGeneric = <T>(message: T) => T;
const helloFunction1: HelloFunctionGeneric = <T>(message: T): T => {
	return message;
}

interface HelloFunctionGeneric2 {
	<T>(message: T): T;
}
const helloFunction2: HelloFunctionGeneric2 = <T>(message: T): T => {
	return message;
}

Generic class

class Person<T, K> {
	private _name: T;
	private _age: K;

	constructor(name: T, age: K) { // constructor에서 T, K가 정의됨
		this._name = name;
		this._age = age;
	}
}

new Person("Gu", 12);
new Person<string, number>('gu', 23);

Generics with extends(제약 조건)

generic에서 extends는 extends로 정해준 타입만 가능하다는 뜻

class PersonExtends<T extends string | number> {
	private _name: T;

	constructor(name: T) {
		this._name = name;
	}
}

new PersonExtends('mark');
new PersonExtends(123);
new PersonExtends(true); //오류 발생

조건부 타입(Conditional Types)

  • 삼항 연산자를 사용하는 타입 부분을 조건부 타입이라고함
    type MyType<T> = T extends string | number ? boolean : never;
    
    const a: MyType<string> = true; // MyType이 boolean이 됨
    const b: MyType<number> = true; // MyType이 boolean이 됨
    const c: MyType<null> = true; ; // MyType이 never가 되므로 에러 발생

유틸리티 타입(Utility Type)

type MyExclude<T, U> = T extends U ? never : T;
type MyUnion = string | number | boolean | null;

const a: MyExclude<MyUnion, boolean | null> = 123; // a라는 변수는 string이거나 number이여야할 때 사용함
// string, number를 제외한 나머지 타입은 never가 됨

// TS는 이미 내장된 Exclude 타입이 존재함
const a: Exclude<MyUnion, boolean | null> = 123;

keyof & type lookup system

  • keyof: 개체의 key 부분만 추출해서 유니온 타입으로 만들어줌
interface IPerson {
	name: string;
	age: number;
}

const person: IPerson = {
	name: 'Gu',
	age: 12,
};

// IPerson[keyof IPerson]
// => IPerson['name' | 'age']
// => IPerson['name'] | IPerson['age']
// => string | number
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
	return obj[key];
}

getProp(person, 'name');

function setProp<T, K extends keyof T>(
	obj: T, 
	key: K, 
	value: T[K]
	): void {
	obj[key] = value;
}

setProp(person, 'name', 'Mark');

infer(추론 키워드)

조건부 타입에서 타입 변수를 추론할 때 사용하는 키워드

type ArrayItemType<T> = T extends (infer I)[] ? I : never;

const numbers = [1, 2, 3];
const a: ArrayItemType<typeof numbers> = 123; // I가 number타입이 됨
const b: ArrayItemType<boolean> = 123; // 에러 발생

const fruits = ['apple', 'banana', 'melon'];
const hello = () => {};
const c: ArrayItemType<typeof fruits> = 'ABC'; // I가 string타입이 됨
const d: ArrayItemType<typeof hello> = 'ABC' // 에러 발생
type SecondArgumentType<T> = T extends (f: any, s: infer S) => any ? S : never;

function hello(a: string, b: number) {}
const a: SecondArgumentType<typeof hello> = 123;
type MyReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

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

const a: MyReturnType<typeof add> = 'hello';

// ReturnType이라는 내장 유틸리티 타입이 있음
const a: ReturnType<typeof add> = 'hello';

타입 가져오기와 내보내기

  • import type 키워드를 통해 타입이라는 것을 명시함
  • namespace를 사용해 type에도 namespace를 적용할 수 있음
export namespace Utils {
  export interface Add {
    // namespace 안에서도 export 키워드가 필요함
    (a: number, b: number): number;
  }

  export interface Subtract {
    (a: number, b: number): number;
  }
}

export const add: Utils.Add = (a, b) => a + b;
export const subtract: Utils.Subtract = (a, b) => a - b;

export default {
  name: 'My utils!',
  add,
  subtract,
};
import utils, { add, subtract } from './myUtils'; // // 데이터로 취급
import type { Utils } from './myUtils'; // 타입으로 취급
// import { add, subtract, Add, Subtract } from './myUtils';
// 데이터와 타입을 섞어서 import하는 것도 가능하지만 직관적으로 구분해서 표현하는 것이 좋음

const a = add(4, 7);
const b = subtract(9, 6);

const c = utils.add(4, 7);
const d = utils.subtract(9, 6);

console.log(utils.name); // 'My utils!'

const newAdd: Utils.Add = (x, y) => x + y;
newAdd(1, 2);

const newSubtract: Utils.Subtract = (x, y) => x - y;
newSubtract(3, 1);

tsconfig.json 옵션

  • root 경로에 tsconfig.json 파일을 생성하여 옵션들을 설정할 수 있음

최상위 프로퍼티

  • compileOnSave
  • compileOptions
  • files
  • include
  • exclude
  • extends
  • references

compileOnSave

{
  ...,
  "compileOnSaveDefinition": {
    "properties": {
      "compileOnSave": {
				//save하면 컴파일을 활성화함
        "description": "Enable Compile-on-Save for this project.",
        "type": "boolean"
      }
    }
  },
  ...,
}

//따라서 아래와 같이 지정하면 파일을 save하면 자동 컴파일이 됨
{
	"compileOnSave": true,
}

compilerOptions

ts파일을 js파일로 변환할 때 어떤 옵션을 사용해서 해석할 것인지 지정

strict

엄격한 타입 검사를 활성화 하는 옵션, 아래의 옵션들을 자동으로 활성화함(기본 값: false)

  • --noImplicitAny
  • --noImplicitThis
  • --strictNullChecks
  • --strictFunctionTypes
  • --strictPropertyInitialization
  • --strictBindCallApply
  • --alwaysStrict

--noImplicitAny

명시적이지 않게 any타입을 사용하여, 표현식과 선어에 사용하면 에러 발생

  • 타입스크립트가 추론에 실패한 경우, any가 맞으면 any라고 지정
  • 아무것도 쓰지 않으면 오류 발생

--noImplicitThis

명시적이지 않게 any타입을 사용하여 this 표현식에 사용하면 에러를 발생

--strictNullChecks

null 및 undefined 값이 모든 유형의 도메인에 속하지 않으며, 그 자신을 타입으로 가지거나, any일 겨우에만 할당이 가능

  • 한 가지 예외로 undefined에 void 할당이 가능

strictNullChecks를 적용하지 않으면 모든 타입은 null, undefined 값을 가질 수 있음. (string으로 타입을 지정해도 null, undefined값을 할당할 수 있음)

--strictFunctionTypes

엄격한 함수의 매개변수 타입 검사

--strictPropertyInitialization

정의되지 않은 클래스의 속성이 생성자에서 초기화되었는지 확인

이 옵션을 사용하려면 --strictNullChecks를 사용하도록 설정해야함

--strictBindCallApply

Function의 내장 함수인 bind/call/apply를 사용할 때, 엄격하게 체크하도록 하는 옵션

  • bind는 해당 함수 안에서 사용할 this와 인자를 설정해주는 역할
  • call과 apply는 this와 인자를 설정한 후, 실행까지 함
  • call vs apply
    • call은 함수의 인자를 여러 인자의 나열로 넣어서 사용하고, apply는 모든 인자를 배열 하나로 넣어서 사용

--alwaysStrict

각 소스파일에 대해 JS의 strict mode로 코드를 분석하고, “엄격하게 사용”을 해제

target & lib

  • target
    • 빌드의 결과물을 어떤 버전으로 할 것인지를 정함
    • ESNext: 가장 최신의 JS 버전을 뜻함
  • lib
    • 기본 type definition 라이브러리를 어떤 것을 사용할 것이지 정함
    • lib을 지정하지 않으면 target의 버전에 맞는 디폴트 값이 있음
    • lib을 지정하면 lib 배열로만 라이브러리를 사용함

module

  • 사용할 모듈 방식을 지정할 수 있는 옵션
  • ESM 방식을 사용하기 위해선 ES2015(ES6) 버전 이상을 사용해야함

moduleResolution

  • 컴파일러가 사용할 모듈 생성 방식 지정
  • node나 bundler 속성을 추천함 ⇒ index.ts 파일을 import 할 때 파일 이름을 생략하고 경로까지만 적어도 import가 가능해짐

paths

  • 경로 별칭 지정
  • 예시
    // tsconfig.js
    {
      "compilerOptions": {
        "paths": {
          "myUtils": ["./my_module/utils.ts"],
    			"~/*": ["./src/*"]
        }
      },
    }
    // main.ts
    import { add } from 'myUtils';
    import { add } from '~/my_modules/utils' // === ./src/my_modules/utils

jsx

  • jsx 출력 방식 제어

outDir, outFile, rootDir

  • outDir: 컴파일 후 생성되는 js 파일이 저장될 폴더명
  • outFile: 복수 파일을 묶어 하나의 파일로 출력 설정
  • rootDir: 시작하는 루트 폴더

files, exclude, include

  • files: ts를 js로 컴파일할 때 어떤 파일을 사용할 것인지 지정
  • exclude: 컴파일에 제외할 폴더들을 지정
  • include: 컴파일에 포함할 폼더들을 지정
  • 우선순위
    • files, exclude, include 순으로 우선순위가 높음
      ⇒ exclude에 포함되는 파일이여도 files에 명시되어 있으면 js파일로 변환됨
    • 셋다 설정이 없으면 전부 다 컴파일함

extends

  • 다른 파일에 있는 옵션을 확장할 때 사용하는 키워드
  • extends와 중복되는 옵션은 tsconfig.json의 옵션 설정으로 덮어씌어짐

내장 유틸리티 타입

Partial

  • 모든 필수 속성들을 선택 속성으로 만들때 사용, ?로 만드는 것과 비슷함
interface User {
  name: string,
  age: number,
}

// error
const userA: User = {
  name: 'A'
}

// 에러가 발생하지 않음
const userB: Partial<User> = {
  name: 'B'
}

Required

  • 모든 선택 속성들을 필수 속성으로 변경할 때 사용
interface User {
  name?: string,
  age?: number,
}

// 에러가 발생하지 않음
const userA: User = {
  name: 'A'
}

// error
const userB: Required<User> = {
  name: 'B'
}

Readonly

  • 모든 속성을 읽기 전용 속성으로 바꿈
interface User {
  name?: string;
  age?: number;
}

const userA: User = {
  name: 'A',
  age: 12,
};

const userB: Readonly<User> = {
  name: 'B',
  age: 12,
};

userA.name = 'AA';
userB.name = 'BB'; // error

Record

  • Record<U, T>
    • U: Union 타입
    • T: Union 타입의 속성이 어떤 타입이 될 것인지 명시
type Names = 'neo' | 'gugu';

const developers: Record<Names, number> = {
  neo: 12,
  gugu: 13,
}

// Record<Names, number>를 통해 만들어지는 타입은 RecordNames와 동일함
type RecordNames = {
  neo: number
  gugu: number
}

Pick

  • 하나의 객체 타입에서 원하는 속성으로 새로운 객체 타입을 만들 수 있음
interface User {
  name: string;
  age: number;
  email: string;
}

// Pick<User, 'name' | 'email'>의 결과는 PickUser와 동일함
const user: Pick<User, 'name' | 'email'> = {
  name: 'gugu',
  email: 'gugu@gugu.com',
  age: 11, // error
};

interface PickUser {
  name: string;
  email: string;
}

Omit

  • Pick과 반대되는 개념
interface User {
  name: string;
  age: number;
  email: string;
}

// Omit<User, 'name' | 'email'>의 결과는 OmitUser와 동일함
const user: Omit<User, 'name' | 'email'> = {
  age: 11,
};

interface OmitUser {
  age: number
}

Exclude

  • Union 타입에서 특정 타입(Union도 작성 가능)을 제외할 때 사용
type T = string | number | boolean;

const a: Exclude<T, number> = 'string';
const b: Exclude<T, number> = 123; // 에러 발생

Extract

  • Extract<T, U>: T라는 Union타입에서 U와 일치하는 타입을 추출하여 새로운 Union타입을 반환함
type T = string | number | boolean;
type U = number | boolean | string[];

const a: Extract<T, U> = 123
// Extract<T, U> === number | boolean

Return

어떤 타입이 반환되는지 알아냄

function hello(msg: string) {
  return msg;
}

const a: ReturnType<typeof hello> = 'string'

Awaited

  • await를 사용해서 반환되는 데이터의 타입을 사용할 수 있음
(async () => {
  const promise = Promise.resolve(true);
  console.log(await promise); // true
  const a: Awaited<typeof promise> = false;
})();
profile
컴퓨터공학과 학생입니다

1개의 댓글

comment-user-thumbnail
2023년 7월 26일

굉장히 정리가 잘 되어있네요!!
잘 참고해서 사용해보겠습니다 👍

답글 달기