나는 오늘도 멋있다

타입스크립트(TypeScript) 본문

Web/TypeScript

타입스크립트(TypeScript)

나는 오늘도 멋있다 2023. 11. 2. 10:22

요즘 정신이 말짱하다. 학습하는대로 내머리에 다들어오는데 누군가가 머리를 계속 감겨주는 것 같은 느낌이다.

그래서 TypeScript를 좀더 알아보려고한다. 어느순간부터 TypeScript를 쓰고있었고 React에 타입을 명시하기 바빳다.

이런 기분에 학습하면 좀더 TypeScript를 더 다룰수 있을까해서 급하게 적어본다.

지금 당장 Google에만 TypeScript란 이라고 치면 엄청나게 많은 블로그들이 많이 뜬다. 그래서 나는 나처럼 깊게 알지 못하는 사람이
있을 수 있다고 생각하여 공식문서를 봤다.

 

"프로그래머들이 작성하는 가장 흔한 오류는 타입 오류입니다: 다른 종류의 값이 예상되는 곳에 특정한 값이 사용된 경우입니다. 이는 단순한 오타, 라이브러리 API를 이해하지 못한 것, 런타임 동작에 대한 잘못된 가정 또는 다른 오류 때문일 수 있습니다. TypeScript의 목표는 JavaScript 프로그램의 정적 타입 검사자 입니다. 즉, 코드가 실행되기 전에 실행하고(정적), 프로그램 타입이 정확한지 확인하는 도구(타입 검사)입니다."

[글 출처]

 

Handbook - The TypeScript Handbook

Your first step to learn TypeScript

www.typescriptlang.org

 

라고한다. 나도 사용할때는 Type에 대한 오류를 줄여주고, 에러를 줄이기 위해 사용한다. 라고 생각했다. 즉 공식문서도 타입에 대한 에러를 줄이기라고 강조하고있다. 그럼 공식문서를 보면서 기본적인 것 부터 다시볼예정이다.

[JavaScript 타입]

 

JavaScript의 타입과 자료구조 - JavaScript | MDN

모든 프로그래밍 언어에는 내장된 자료구조가 존재하지만, 보통 그 내용은 언어마다 다릅니다. 이 글에서는 JavaScript에서 사용할 수 있는 내장 자료구조와 그 속성에 대해 알아보겠습니다. 그러

developer.mozilla.org

 

1. 기본적인 타입들의 타입추론 기능(Types by Inference)

let num = 1;
// let num:number

let bol = true;
// let bol:boolean

let str = "test";
// let str:string

let arr = [1,2,3];
// let arr:number[]

let obj = {key: 1};
// let obj:{key:number}

let nul = null;
// let nul:any

TypeScript는 JavaScript 언어를 알고있어서 대부분의 경우 타입을 인식한다. 즉 변수같은 경우에는 굳이 옆에다가 타입을 적지 않아도 된다는것이다. 또한 JavaScript에서는 let을 선언과 동시에 number을 할당하고 밑에서 다시 string을 선언하면 문제가없지만 TypeScript 에서는 오류가 난다.(실행은 가능함)

X : let num = 1; 
// let num: number;

num = "test"
// 'string' 형식은 'number' 형식에 할당할 수 없습니다.

O : let num: string | number = 1;
// let num: string | number

num = "test"
// 유니온타입으로 오류가 발생하지않음

이럴경우에는 유니온타입(or) 으로 명시 해주는것이 좋다. 

 

2.-1 interface : 주로 객체의 구조 정의, 클래스 와 객체의 상호작용을 정의하는데 사용된다

앞서 말했다시피 타입추론기능으로 인해 객체를 생성해도 TypeScrip는 타입추론기능이 있어서 객체의 프로퍼티 타입을 인식한다.

let obj1 = {
    key1: 1,
    key2: 2,
    key3: "test"
  }
  
  //let obj1: {
    key1: number;
    key2: number;
    key3: string;
}

// 타입을 자동으로 추론한다.

위처럼 문제는 없지만 좀더 명식적으로, 가독성위해서, 리팩토링을 쉽게하기위해, 문서화하여 협업할때 객체의 예상 구조를 파악하기 쉽게하기위해서 등등 많은 이유가 있다. 그래서 interface: typename 을 사용할수가 있다.

interface Obj1{
    key1: number;
    key2: number;
    key3: string;
  }


  let obj1: Obj1 = {
    key1: 1,
    key2: 2,
    key3: "test"
  }

위처럼 객체의 프로퍼티명 과 일치하도록 작성해야하며, 해당 인터페이스에 맞지않는 객체를 생성하면 경고를 보여준다.

 

2-2 Interface 와 함수(function)

interface는 함수에서 매개변수와 리턴값을 명시하는데도 사용된다.

[함수의 매개변수 타입을 명시]

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

  function people1( {age, name} : People1Type){
    console.log(age, name);
  }

[함수의 리턴타입을 명시]

interface People2Type{
    value: string;
  }

  
  function people2(): People2Type{
    const value = "사람";
      return {value};
  }

리턴타입을 명시하는 과정에서는 interface는 객체를 명시하고 있기 때문에 객체리터럴로 반환하고 있다. 만약 value변수를 반환하는것이라면 리턴타입을 명시하는곳에 string만 입력해주어도 된다. 여기서! 리턴타입또한 명시하지않아도 일반적인 변수처럼 타입추론기능이 있다. 명시하지않아도 return Type을 추론하기도하고, 반환하는 값이없다면 :void로 추론을한다. 하지만 위해서 TypeScript를 쓰는이유를 생각해보면 작성하는것이 맞는 것 같다.

 

[다양한 함수 활용 방식들] : 화살표함수, 함수표현식, 선언식함수 동일하다

// 첫번째 함수: 객체 구조 분해 방식
  function people( data: {age: number, name: string}): string | void{
    const {age, name} = data;

    if(age < 20){
      return "20세미만은 입장할수 없습니다.";
    } else if(20 <= age && age <= 30){
      return `${name}님 안녕하세요 올해 ${age}이네요`;
    }
    return;
  }
  console.log(people({age:40, name:"홍길동"}));

  
  // 두번째 함수: 인터페이스를 사용한 객체 구조 분해 방식
  interface People1Type{
    age: number;
    name: string;
  }

  function people( {age, name} : People1Type): string | void{
    if(age < 20){
      return "20세미만은 입장할수 없습니다.";
    } else if(20 <= age && age <= 30){
      return `${name}님 안녕하세요 올해 ${age}이네요`;
    }
    return;
  }
  console.log(people({age:40, name:"홍길동"}));

  // 세번째 함수: 개별 매개변수 방식
  function people( age: number, name: string): string | void{
    if(age < 20){
      return "20세미만은 입장할수 없습니다.";
    } else if(20 <= age && age <= 30){
      return `${name}님 안녕하세요 올해 ${age}이네요`;
    }
    return;
  }
  console.log(people(40, "홍길동"));


  // 네번째 함수: 배열 구조 분해 방식 1
  function people(params: [number, string]): string | void {
    const [age, name] = params;
  
    if (age < 20) {
      return "20세미만은 입장할수 없습니다.";
    } else if (20 <= age && age <= 30) {
      return `${name}님 안녕하세요 올해 ${age}이네요`;
    }
    return;
  }
  console.log(people([29, "홍길동"]));

  // 다섯번째 함수: 배열 구조 분해 방식 2
  function people([age, name]: [number, string]): string | void {
    if (age < 20) {
      return "20세미만은 입장할수 없습니다.";
    } else if (20 <= age && age <= 30) {
      return `${name}님 안녕하세요 올해 ${age}이네요`;
    }
    return;
  }
  console.log(people([20, "홍길동"]));

 

3. 타입 구성 (Composing Types) : 주로 유니온, 인터섹션 별칭 등을 통해 타입을 조합 or 변환에 사용된다.

유니온(Union)

type ageType = string | number;

  function People(age: ageType): void{

    console.log(`나이는:${age} 입니다.`)
  }

  People(10);
  
//  type typaName = type | type 처럼 정의한 타입으로도 사용이 가능하다.

인터섹션(Intersection)

 type log = {
  simpleLog: () => void;
 }

 type messageLog = {
  messageLog: (name: string) => void;
 }

 type objLogType = log & messageLog;

 const objLog: objLogType = {
  simpleLog: ()=> console.log("안녕하세요"),
  messageLog: (name)=>console.log(`${name}님 안녕하세요`)
 }

 objLog.simpleLog();
 objLog.messageLog("홍길동")

제네릭(Generics)

interface 및 type

interface Box<T> {
    value: T;
  }

  type Box1<T> = {
    value: T;
  }
  
  const numberBox: Box<number> = { value: 42 };
  const stringBox: Box1<string> = { value: "Hello" };

 

클래스(Class)

 class Container<T> {
    constructor(public value: T) {}
  
    getValue(): T {
      return this.value;
    }
  }
  
  const numberContainer = new Container<number>(42);
  const stringContainer = new Container<string>("Hello");

 

선언식함수

function identity<T>(arg: T): T {
    return arg;
  }
  
  const result = identity(42)

 

함수표현식

const identity = function<T>(arg: T): T {
    return arg;
  }
  
  const result = identity(42);

 

화살표함수

const identity = <T>(arg: T): T => {
  return arg;
}

const result = identity<number>(42);

 

화살표함수를 .tsx 확장자에서 제네릭를 사용할경우 에는 일반적인 방법으로 사용할수가 없다. 선언식함수, 함수표현식은 function키워드와 포함되어있기 때문에 컴파일러가 이를 태그로 인식을 한다. 조금은 다른 방식을 사용해야한다.

 

화살표함수 .ver: tsx

// 매개변수를 두개사용
  const identity1 = <T,S>(arg: T): T => {
    return arg;
  }
  const result1 = identity1(42);


// null 과 undefined 형식은 받을수 없다.
  const identity2 = <T extends {}>(arg: T): T => {
    return arg;
  }
  const result2 = identity2(42);

  // 특별타입 사용: 알려지지 않은타입이며 모든타입의 최상위 타입
  const identity3 = <T extends unknown>(arg: T): T => {
    return arg;
  }
  const result3 = identity3(42);

  // ,를 사용
  const identity4 = <T,>(arg: T): T => {
    return arg;
  }
  const result4 = identity4(42);

 

4. 덕 타이핑(duck typing) or 구조적 타이핑

문서를 읽다가 새로운 사실을 발견했다. 
"TypeScript의 핵심 원칙 중 하나는 타입 검사가 값이 있는 형태에 집중한다는 것입니다. 이는 때때로 “덕 타이핑(duck typing)” 또는 “구조적 타이핑” 이라고 불립니다."

라고한다. 

interface Point {
  x: number;
  y: number;
}

function printPoint(p: Point) {
  console.log(`${p.x}, ${p.y}`);
}
// ---cut---
const point3 = { x: 12, y: 26, z: 89 };
printPoint(point3); // prints "12, 26"

const rect = { x: 33, y: 3, width: 30, height: 80 };
printPoint(rect); // prints "33, 3"

위처럼 객체의 값중 해당하는 매개변수에 해당하는 값을 받는것이다. 말그대로 형태 와 동작에 집중하는것이다.

 

[참고한 내용들]

 

Documentation - TypeScript for JavaScript Programmers

Learn how TypeScript extends JavaScript

www.typescriptlang.org

 

interface 와 type의 다양한 사용법

interface 와 type 정말 다양한 사용법들이 있다. 실제로 학습프로젝트를 하다가  interface or type 으로 어떻게 정의를 해줘야하는지 어려움을 겪은 적도있다.

 

1. 상속(extends) 및 확장

interface Age{
  age: number;
}

interface Name{
  name: string;
}

interface PeopleType extends Age, Name {
  gender: string;
}

let people: PeopleType = {
    age: 20,
    name: "홍길동",
    gender: "남"
  }

 

2. 선택적 속성(Optional Properties) : type도 가능

interface Age{
  age: number;
}

interface Name{
  name: string;
}

interface PeopleType extends Age, Name {
  gender?: string;
}

 let people: PeopleType = {
    age: 20,
    name: "홍길동",
  }
  
  // gender 속성은 옵셔널로 지정되어 있기때문에 string | undefinde 이다

 

3. 읽기 전용 속성(Readonly Properties) : type도 가능

interface Game {
    readonly HP: number;
    readonly MP: number;
    readonly name: string;
  }

  let people: Game = {HP:100, MP:100, name:"이름설정"};
  people.HP = 150; // Error


  let user1: Game = {...people, name:"뉴비"};
  // 새로운 객체에 복사해서 값을 변경

 

4. 함수 시그니처(Function Signatures) : type도 가능

* 함수시그니처는 화살표함수, 함수표현식에서만 사용가능하다. 콜백함수나 이벤트 핸들러와 같은 상황에서 함수 형태를 정의하는데 사용된다. 선언적함수는 함수자체가 시그니처가 되기때문에 사용이 불가능하다.
* 시그니처 란?: 함수가 어떤형태로 호출될 수 있는지를 정의하는것이며, 함수 시그니처에는 함수이름,매개변수,반환타입 및 함수 스코프 등이 포함될수있다. 즉 함수를 어떻게 사용해야 하는지에 대한 규칙이다. 

interface PeopleType{
    (name: string, age: number): {name: string, age: number} | string;
  }
  
type PeopleType1 = (name: string, age: number) => {name: string, age: number} | string;


// 화살표함수 예시
  let ArrowFunction:PeopleType = (name, age)=>{

    if(20 > age){
      return "나이가 어립니다"
    }

    return {name, age};
  }

// 함수표현식 예시
  let FunctionExpression:PeopleType1 = function(name, age){

    if(20 > age){
      return "나이가 어립니다"
    }

    return {name, age};
  }

 

5. 인덱서 (Index Signatures)

interface Dictionary {
    [key: string]: string;
  }
  
  const myDictionary: Dictionary = {
    key1: "value1",
    key2: "value2",
    key3: "value3",
  };
  
  const value = myDictionary["key1"]

 

 

6. 하이브리드 타입(Hybrid Types)

interface Counter {
    value: number;
    increment: () => void;
    call: (num: number) => void;
  }


  const myObject: Counter = {
    value: 0,
    increment: function() {
      this.value++;
    },
    call: function(num){
       this.value += num;  
    }
  }

 

 

함수별 타입선언방식

// 화살표함수 기본
const arrow1 = (x:number, y:number): number => x + y;

// 화살표함수 함수시그니처 interface
interface ArrowInterface{
  (x:number, y:number) : number;
}
const arrow2:ArrowInterface = (x, y) => x + y;

//화살표함수 함수시그니처 type
type ArrowType = (x:number, y:number) => number;
const arrow3:ArrowType = (x, y) => x + y;

//화살표함수 함수시그니처 직접사용
const arrow4: (x:number, y:number) => number = (x, y) => x + y;

//화살표 함수 제네릭
type ArrowGeneric<T> = (x: T, y:T) => T;
const arrow5: ArrowGeneric<number> = (x, y) => x + y;

// 화살표함수 반환타입
const arrow6 = ()=>{
  return "arrow"
}
type ArrowReturn = ReturnType<typeof arrow6>;

--------

//함수 표현식 기본
const expression1 = function(x:number, y:number): number{
  return x + y;
}

// 함수표현식 함수시그니처 interface
interface ExpressionInterface{
  (x:number, y:number) : number;
}
const expression2:ExpressionInterface = function(x, y){
  return x + y;
}

//함수표현식 함수시그니처 Type
type ExpressionType = (x:number, y:number) => number;
const expression3:ExpressionType = function(x, y){
  return x + y;
}

//함수표현식 함수시그니처 직접사용
const expression4: (x:number, y:number) => number = (x, y) => x + y;

//함수표현식 함수 제네릭
type expressionGeneric<T> = (x:T, y:T) => T;
const expression5:expressionGeneric<number> = function(x, y){
  return x + y;
}

// 화살표함수 반환타입
const expression6 = ()=>{
  return "expression"
}
type ExpressionReturn = ReturnType<typeof expression6>;


--------

//함수 선언식 기본
function declarations1(x:number, y:number):number{
  return x + y;
}


//함수 선언식 네임드 파라미터
function declarations2({name, age}: {name:string, age:number}): string{
  return `${name}${age}`;
}
declarations2({name: "홍길동", age: 20})

// 함수 선언식 콜백함
const log = function( value: string): void{
  console.log(value);
}

function declarations3(callback:(arg:string) => void){
  callback("console.log");
}
declarations3(log)