[TS] Part2 - 살짝 심화

JH Cho·2023년 1월 11일
0

TypeScript

목록 보기
3/6

함수 rest 파라미터, destructuring 타입 지정

rest parameter

function 함수(...a: number[]) {
  // 여러 파라미터가 배열로 들어옴
  console.log(a);
}

함수(1, 2, 3); //[ 1, 2, 3 ]

spread operator

let arr = [1, 2, 3];
let arr2 = [4, 5];
let arr3 = [...arr, ...arr2];
console.log(...arr3); //1 2 3 4 5

let obj = { a: 'b', b: 'c' };
console.log({ ...obj }); //{ a: 'b', b: 'c' }

MDN spread 문법
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax

destructuring

  • 배열이나 객체의 값들을 변수에 쉽게 할당해준다.
  • let 변수1 = 배열[0]; 이렇게 안해도 되겠지.
let [변수1, 변수2] = ['안녕', 100];
console.log(변수1, 변수2); //안녕 100

// let { student, age } = { student: true, age: 20 };
//사실 정확히는
let { student: student, age: age } = { student: true, age: 20 };
//student변수에는 오른쪽 객체의 student프로퍼티의 값을 할당해주세요~ 이런 의미
console.log(student, age); //true 20

// 응용
interface Info {
  student: boolean;
  age: number;
}

function 함슈({ student, age }: Info) {
  console.log(student, age); //true 20
}

함슈({ student: true, age: 20 });

연습해보기

  • 숫자 넣으면 가장 큰 수 구하기
    Math.max 금지
type FindMax = (...x: number[]) => number;

const findMax: FindMax = (...nums) => {
  let maxNum: number = 0;
  nums.forEach((num) => {
    if (maxNum <= num) maxNum = num;
  });
  return maxNum;
};

findMax(5, 2, 3, 4);

연습2

const 연습2 = ({
  user,
  comment,
  admin,
}: {
  user: string;
  comment: number[];
  admin: boolean;
}) => {
  console.log(user, comment, admin);
};

 연습2({ user: 'kim', comment: [3, 5, 4], admin: false });

연습3

const 연습3 = ([a, b, c]: (number | string | boolean)[]) => {
  console.log(a, b, c);
};
연습3([40, 'wine', false]);


Narrowing 더 알아보기

&& 연산자로 null & undefined 체크!

function 함수(a: string | undefined) {
  // a 가 undefined가 아니고 스트링임을 한 줄로.
  if (a && typeof a === 'string') {
  }
}

in 키워드

type Fish = { swim: string };
type Bird = { fly: string };

function 함슈(animal: Fish | Bird) {
  // animal.swim -> 아직 확실하지 않아서 에러.
  if ('swim' in animal) {
    console.log(animal.swim);
  } else if ('fly' in animal) {
    console.log(animal.fly);
  }
}

인스턴스(객체) instanceof 부모class

참고 인스턴스란?
생성자 함수 또는 클래스가 만들어낸 객체를 의미.

let 날짜 = new Date();

if (날짜 instanceof Date) {
  console.log('날짜는 Date의 자식이에요!');
} //날짜는 Date의 자식이에요!

비슷한 객체 타입이 많을 때?

  • 타입에 literal type을 강제로 만들어두면 도움됨.
type Car = {
  wheel: '4개';
  color: string;
};
type Bike = {
  wheel: '2개';
  color: string;
};

function 예제3(x: Car | Bike) {
  if (x.wheel === '4개') {
    console.log('차에요');
  } else if (x.wheel === '2개') {
    console.log('바이크에요');
  }
}


함수의 never

never
1. 절대 return 하지 않는다.
2. 함수 실행이 끝나지 않는다(endpoint 존재 X)

함수에 never지정해주면 타입 에러가 뜸.
모든 함수에는 return을 직접 지정해주지 않으면
기본적으로 return undefined를 하고 있기 때문.

throw new Error()

function 함수(): never {
  throw new Error();
  // 다음 코드를 실행하기 전에 Error로 인해 멈춤.
}

while

function 함수2(): never {
  while (true) {
    //코드
  }
  // 영원히 끝나지 않는 while 문.
}

어디 써요?

  • 보통 void로 해결되기 때문인데..

never 등장 case 1

  • 그런 경우가 없어!
function 함수3(param: string) {
  if (typeof param === 'string') {
    console.log(param);
  } else {
    console.log(param); // never
  }
  // param은 스트링만 들어올 수 있으니까 never
}

never 등장 case 2

  • 함수 표현식
let 함수4 = function () {
  throw new Error();
};

결론

  • 굳이 사용되는 경우는 없고 never가 뜨면 왜 그런지 이해만


public, private

class 요약 정리

class Case1 {
  name = 'kim';
}

class Case2 {
  name;

  constructor() {
    this.name = 'kim';
  }
}
// Case1, Case2 는 똑같다.
// 하지만 constructor의 파라미터를 이용해서
// 생성되는 인스턴스들의 속성 변경 가능.

public

  • 모든 인스턴스들이 사용 가능.
class User {
  public name = 'kim';

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

  public 프로토타입에추가되는함수() {
    console.log('프로토타입함수');
  }
}

let 유저1 = new User('park');

유저1.name = 'kaka';
console.log(유저1); // kaka

private 키워드

  • 특정 속성을 보호하고 싶을 때
  • 생성자 class에서 직접 조회는 가능함.
class User2 {
  private name = 'kim';
  constructor(a: string) {
    this.name = a;
  }
}

let 유저2 = new User2('park');
console.log(유저2.name);
// (에러문구) property name은 private이라
// User2 클래스 내에서만 수정, 사용이 가능하다.

사용 예시

class Example {
  name: string;
  private familyName: string = 'kim';

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

let 예시 = new Example('호호');
예시.name; // 호호KIM

// familyName은 다른 사람이 변경하면 안될 일!

// 중요 프로퍼티 등은 private를 이용해서
// 수정 등을 방지

class 밖에서 수정가능하게 할 수 있다?

  • 전국민의 성을 바꾸라는 명령
class Example2 {
  name: string;
  private familyName: string = 'kim';

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

  성씨변경함수(a: string) {
    //프로토타입
    this.familyName = a;
  }
}

let 시민 = new Example2('봄봄');
시민.성씨변경함수('park');
console.log(시민);
//Example2 { familyName: 'park', name: '봄봄kim' }

public 쓰면 this. 생략 가능

  • 기존
class Person {
  name;
  constructor() {
    this.name = 'KAK';
  }
}
  • public 이용
class Person2 {
  constructor(public name: string) {
    // 이 자리의 파라미터는 자식의 name 속성에 할당해주세요
    // => feild에 name과 컨스트럭터에 this.name = name 생략 가능.
  }
}

let person1 = new Person2('이름');
console.log(person1);
//Person2 { name: '이름' }


class extends & protected, static

extends

class User {
  x = 10;
}

class Copy extends User {}

const CopyUser = new Copy();
console.log(CopyUser); //Copy { x: 10 }

protected

  • extends 된 클래스는 사용가능
  • 생성된 인스턴스는 사용 불가능!
// private은 클래스나 인스턴스나 사용 불가능.
class Private {
  private x = 10;
}
// extends 클래스에서 x 사용 불가.
class NewPrivate extends Private {}

class Protect {
  protected x = 10;
}
// extends 클래스 내에서 x 사용 가능.
class NewProtec extends Protect {
  doThis() {
    this.x = 20;
  }
}

static

  • static은 부모 클래스에 직접 부여되며 인스턴스에 물려주지 않음.
class Uer {
  static x = 10;
  y = 20;
}

let 자식 = new Uer();
console.log(자식); // Uer { y: 20 }
  • x 는 부모만
    console.log(Uer.x); //10

  • y는 부모가 못쓰고 자식만.
    console.log(Uer.y); //undefined

  • private / protected / public 과 동시 사용 가능

class Uer2 {
  private static x = 10;
  y = 20;
}

class Apply {
  static skill = 'JS';
  intro = `${Apply.skill} 전문가입니다.`;
  // this.skill 에러
  // static은 부모만의 프로퍼티
}

const 철수 = new Apply();
console.log(철수);
// Apply { intro: 'JS 전문가입니다.' }

Apply.skill = 'TS';

const 영희 = new Apply();
console.log(영희);
// Apply { intro: 'TS 전문가입니다.' }

연습 1

class 연습 {
  private static x = 10;
  public static y = 20;
  protected z = 30;
}
/*
1. 필드값은 원래는 모든 User의 자식들에게 물려주는 속성이지만 

x와 y에는 static 키워드가 붙었기 때문에 User.x 이런 식으로만 접근해서 쓸 수 있습니다.

User의 자식들은 x와 y를 쓸 수 없습니다.

 

2. private static x는 class 내부에서만 수정가능합니다. 

 

3. public static y는 class 내부 외부 상관없이 수정가능합니다. public 키워드 지워도 똑같이 동작할 듯 

 

4. protected z는 private 키워드와 유사하게 class 내부에서만 사용이 가능한데 

약간 범위가 넓어서 extends로 복사한 class 내부에서도 사용할 수 있습니다. 
*/
class 연습확장 extends 연습 {}

const practice = new 연습();
console.log(연습);
console.log(practice);

연습2

class 연습2 {
 private static x = 10;
 public static y = 20;

 addOne(num: number) {
   연습2.x += num;
 }
 printX() {
   console.log(연습2.x);
 }
}

연습3

class Square {
 x;
 y;
 color;
 constructor(a: number, b: number, c: string) {
   this.x = a;
   this.y = b;
   this.color = c;
 }
 draw() {}
}
let 네모 = new Square(30, 30, 'red');

// 답
class Square {
 constructor(
   public width: number,
   public height: number,
   public color: string
 ) {}
 draw() {
   let a = Math.random();
   let square = `<div style="position:relative; 
     top:${a * 400}px; 
     left:${a * 400}px; 
     width:${this.width}px; 
     height : ${this.height}px; 
     background:${this.color}"></div>`;
   document.body.insertAdjacentHTML('beforeend', square);
 }
}

let 네모 = new Square(30, 30, 'red');
네모.draw();
네모.draw();
네모.draw();
네모.draw();


import export namespace

//export(A.ts)
export let 변수 = 'kim';
export let 변수2 = 20;

export type Name = string;
export interface 인터페이스 {}

namespace 네임스페이스 {
  export type Name = string | number;
}

module 네임스페이스와같음 {
  
}

// import(index.ts)
import { 변수, 변수2, Name} from './a';

console.log(변수, 변수2); //kim 20

let 이름: Name = 'park';

// import, export가 있기 전엔
// <script src='a.js />
// 이런식으로 불러왔음.
// 해당 소스 파일의 모든 코드를 불러오기 때문에
// 변수가 겹치는 등의 문제가 발생함.
// 그래서 그 당시엔 namespace 문법을 사용함.

///<referenc path='./a.ts' />
네임스페이스.Name;


Generic

function 함수(x: unknown[]) {
  return x[0];
}

let a = 함수([4, 2]);
// a의 타입 unknown
// return이 숫자라도 자동 타입 변환하지 않음.
console.log(a + 1);
// error ->narrowing 해결 but 불편함.

generic 사용 예

function 제네릭<아무거나>(x: 아무거나[]): 아무거나 {
  return x[0];
}
//파라미터로 타입을 지정 가능.
let b = 제네릭<number>([4, 2]);
console.log(b + 1); // 해결

let c = 제네릭<string>(['4', '2']);

let d = 제네릭([true, false]);
// typeof d === boolean (지정안해줘도 알아서 해줌.)

constraints(제네릭의 타입 제한하기.)

function 함슈<MyType>(x: MyType) {
  return x - 1;
  // 미리 x는 any, bigInt, number 등의 타입만 가능하다고
  // 에러를 띄워줌.
}
// narrowing 번거로우니
function 함슈<MyType extends number | bigint>(x: MyType) {
  return x - 1;
}
let result = 함슈<number>(100);

커스텀 타입도 constraints 가능

interface LengthCheck {
  length: number;
}

function lengthChecker<MyType extends LengthCheck>(x: MyType) {
  return x.length;
}
let fw = lengthChecker<string>('hi');
let f = lengthChecker<number>(1234);

class 제너릭<MyType> {
  a: MyType;
  constructor(x: MyType) {
    this.a = x;
  }
}
const 뉴클 = new 제너릭<number>(2);

console.log(뉴클);
//제너릭 { a: 2 }

연습1

//문자를 집어넣으면 문자의 갯수, array를 집어넣으면 array안의 자료 갯수를 콘솔창에 출력해주는 함수

function 함수<T extends string | string[]>(x: T) {
  console.log(x.length);
}

연습2

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

let data = '{"name" : "dog", "age" : 1 }';

function convert<T extends Animal>(x: string): T {
  return JSON.parse(x);
}
// 또는 extends 안쓰고
convert<Animal>(data);

연습3

class Person<T> {
  name;
  constructor(a: T) {
    this.name = a;
  }
}
let a = new Person<string>('어쩌구');
a.name; //any 타입이 아닌 제너릭에 따라 타입지정되도록.


tuple type

let dog: (string | boolean)[] = ['dog', true];
// 첫 인덱스 요소는 스트링 두번째는 불리언!
let dog1: [string, boolean] = ['dog', true];

//옵셔널도 가능.
let cat: [string, boolean?] = ['dog']; //에러 X

//주의
let cat1: [string, boolean?, number];
// 요소 순서가 불확실해져서 옵셔널은 마지막 요소만

함수 tuple(rest - parameter)

// number[] 대신 tuple도 가능.
function 함수(...x: [number, number, number, number]) {
  console.log(x);
}
함수(1, 2, 3, 4);

array 합칠 때(spread operator)

let arr = [1, 2, 3];
let arr2 = [4, 5];
let combine = [...arr, ...arr2];

//이렇게 쓰면 됨.
let tuple: [number, number, ...number[]] = [4, 5, ...arr];


declare & ambient module

.js 파일의 변수를 .ts에서 이용하기

declare

  • 어딘가 분명 변수 a가 있으니 에러내지 말아달라는 의미.
  • 변수 재정의가 가능하다.
  • declare는 js로 컴파일되지 않음.
  • js로 만든 라이브러리를 TS에서 사용할 때 많은 에러가 발생하기 때문에
    변수나 함수를 declare로 재정의 하는 경우가 많음.
// .js
var a = 10;
var b = { name: 'kim' };

// .ts
declare let a: number;
console.log(a + 1);

ambient module(글로벌 모듈)

ts -> ts는?

  • import export하면 됨.
  • ts의 특징 : 모든 ts 파일은 ambient module(글로벌 모듈)
  • 그래서 import export 없어도 global 변수선언이 되어
  • ts 끼리 서로 변수를 사용 가능함.

let name // error 뜨는 이유
ts기본 파일에 이미 let name이 있기 때문.

ambient module 예시

//data.ts
var a = 10;

// index.ts
console.log(a); // error X

let a; // cannot redeclare error

로컬 모듈로 만들어버리기

//data.ts
var a = 10;
export {}
// index.ts

let a; //good

로컬모듈에 글로벌 변수도 쓰고싶다

  • declare global {}
// data.ts
let a = 10;
declare global {
  type Dog = string;
}

export { a };
// index.ts
import { a } from './data.js';
console.log(a + 1);

let b: Dog = 'kim';

d.ts 파일 이용하기

<파일명>.d.ts 파일의 사용처

  1. 타입정의만 따로 저장해서 import해서 사용
  2. 프로젝트에서 사용하는 타입을 쭉 정리할 레퍼런스용

import해서 사용하기

// ## 파일명.d.ts
// 타입정의 보관용 파일

export type Age = number;
export interface Person {
  name: string;
}

// ## index.ts

import { Age } from './test';
// 또는
import * as 변수명 from './test';

let age: Age;

d.ts 파일을 레퍼런스용으로 사용하기

  • declaration 옵션을 true로 설정하면
    ts파일 저장시 자동으로 d.ts파일이 생성됨.
// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "noImplicitAny": true,
    "strictNullChecks": true,
    "declaration": true
  }
}

이렇게 저장된다.

//(index.ts)

let 이름 :string = 'kim';
let 나이 = 20;
interface Person { name : string } 
let 사람 :Person = { name : 'park' }
--
//(index.d.ts)

declare let 이름: string;
declare let 나이: number;
interface Person {
    name: string;
}
declare let 사람: Person;

주의!

  • d.ts파일은 글로벌 모듈이 아니다.
  • 글로벌 모듈로 사용하고 싶다면
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "noImplicitAny": true,
    "strictNullChecks": true,
    "declaration": true,
    "typeRoots": ["./types"]
  }
}
  • typeRoots 폴더 내의 d.ts파일 타입들을 전역으로 사용가능하게 하는 옵션.
// types폴더의 common폴더에 위치한 test.d.ts 파일.

type Age = number;

// index.ts

let 나이: Age = 30;

외부 라이브러리 등을 사용할 때

예)
npm i -save @types/jquery
node modules 파일의 @types에 추가됨.

주의할 점은 typeRoots 옵션을 설정해두면 자동으로 적용이 안되기 때문에
해당 설정을 끄는 것을 권장.

implements

iterface는 객체의 타입을 지정할 때 사용한다.
또한 class의 타입을 확인할 때도 사용하는데 이 때 implements가 필요!

implements

interface CarType {
  model: string;
  price: number;
}

class Car implements CarType {
  model: string;
  constructor(a: string) {
    this.model = a;
  }
}
error: Class 'Car' incorrectly implements interface 'CarType'.
  Property 'price' is missing in type 'Car' but required in type 'CarType'.

!!
implements는 타입을 지정해주지 않고 속성 확인만을 해준다!

interface CarType {
  model : string,
  tax : (price :number) => number;
}

class Car implements CarType {
  model;   ///any 타입됨
  tax (a){   ///a 파라미터는 any 타입됨 
    return a * 0.1
  }
}

object index signatures

// 기존
interface String {
  name: string;
  age: string;
  location: string;
}

//모든 속성을 한번에 타입지정하고 싶을 때

interface String2 {
  [key: string]: string | number;
}

에러나는 경우

interface StringOnly {
  age : number,   ///에러
  [key: string]: string,
}

interface StringOnly {
  age : string,   ///가능  
  [key: string]: string,
}
  
  interface StringOnly {
  age : number,   ///가능
  [key: string]: string | number,
}

array 경우

interface StringOnly {
  [key: number]: string,
}

let obj :StringOnly = {
  0 : 'kim'
  1 : '20',
  2 : 'seoul'
}
// string으로 지정해도 에러 안남.
interface StringOnly {
  [key: string]: string;
}

Recursive Index Signatures

// 기존
interface MyType {
  'font-size': {
    'font-size': {
      'font-size': number;
    };
  };
}

// Recursive Index Signatures
interface MyType {
  'font-size': MyType | number;
}

let css: MyType = {
  'font-size': {
    'font-size': {
      'font-size': 14,
    },
  },
};

사용해보기!

  • index signatures
interface MyObj {
  [key: string]: string | number;
}

let obj: MyObj = {
  model: 'k5',
  brand: 'kia',
  price: 6000,
  year: 2030,
  date: '6월',
  percent: '5%',
  dealer: '김차장',
};
  • Recursive
interface MyObj {
  'font-size': number;
  [key: string]: MyObj | number;
}

let obj: MyObj = {
  'font-size': 10,
  secondary: {
    'font-size': 12,
    third: {
      'font-size': 14,
    },
  },
};

object 타입 변환

Object.keys(obj)

let obj = {
  name: 'kim',
};

console.log(Object.keys(obj));
// 키를 배열로 반환.
//[ 'name' ]

keyof

object의 키를 이용해서 union type을 만들어줌

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

type PersonKeys = keyof Person
 // 'age' 또는 'name' 타입 지정

let a: PersonKeys = 'age';// 'age2' 불가
let b: PersonKeys = 'name';

index signature?

interface Person {
  [key: string]: number;
}

type PersonKeys = keyof Person;
// string | number
// ? key를 string으로 지정해뒀는데..
// obj 키에 number 넣어도 자동으로 스트링으로 변환되기 때문에

타입 변환기 만들기!

// 아래 타입을 모두 string 타입으로 바꾸고 싶을 때?
type Car = {
  color: boolean;
  model: boolean;
  price: boolean | number;
};

// 변환기 만들기
type TypeChanger<MyType> = {
  [key in keyof MyType]: string;
};

type NewType = TypeChanger<Car>;

let a: NewType = {
  color: false, // 에러
};

[key in keyof MyType] : string
keyof MyType // 'color' | 'model' | 'price'
키값이 오른쪽 유니온 타입에 포함된다면 string으로 타입지정해주세요!

모두 적용 아닌 원하는 타입으로 적용하고 싶을 때

type Bus = {
  color : string,
  model : boolean,
  price : number
}

type TypeChanger <MyType, T> = {
  [key in keyof MyType]: T;
};

type NewBus = TypeChanger<Bus, boolean>;
type NewBus2 = TypeChanger<Bus, string[]>

조건부 타입 만들기 & infer

삼항조건식

type Age<T> = T extends string ? T : unknown;
// extends : T 가 string 타입인지 확인

let a: Age<string>; // string 타입
let b: Age<number>; // unknown 타입

활용해보기

// 파라미터로 array 타입 입력하면 array의 첫 인덱스 타입을
// 다른 거 입력하면 any를 타입으로.
type FisrtItem<T> = T extends any[] ? T[0] : any;

let c: FisrtItem<string[]> = '스트링';
let d: FisrtItem<number>; //any

infer

//  기존 : T가 스트링타입이면 T를 아니면 unknown으로 타입지정
type Person<T> = T extends string ? T : unknown;

// infer

type Person2<T> = T extends infer R ? R : unknown;
// T에서 타입을 추출해라

let a: Person2<string>;

// 실용예제- array 내부 타입 추출
type 타입추출<T> = T extends (infer R)[] ? R : unknown;

type a = 타입추출<string[]>;
// T는 string[]으로 들어왔고 R[]이 T를 추출해서 R이 string이 됨.
// a의 타입이 string이 됨.

// 예제2 - 함수 추출
type 타입추출2<T> = T extends () => infer R ? R : unknown;
type b = 타입추출2<() => void>;
// b 타입 void
이거 써도 됨!
// #### ReturnType 
type c = ReturnType<() => void>;

연습해보기

1번

  • array 타입 입력 -> array 첫 자료가 string이면 string타입
    -> 아니면 unknown
type 추출<T> = T extends [string, ...any] ? T[0] : unknown;


let age1: 추출<[string, number]>; // string
let age2: 추출<[boolean, number]>; // unknown

2번

  • 함수의 파라미터 타입을 추출
type 추출<T> = T extends (x: infer R) => any ? R : any;

let a: 추출<(x: number) => void>;
// number
let b: 추출<(x: string) => void>;
// string
profile
주먹구구식은 버리고 Why & How를 고민하며 프로그래밍 하는 개발자가 되자!

0개의 댓글