IT/Typescript

타입스크립트 정리

라임웨일 2022. 9. 8. 15:32
반응형

📖 타입스크립트를 사용해야 하는 이유

⭐ Javascript

  • 자바스크립트는 매우 유연한 언어라 에러를 사전에 보여주지 않기 때문에 코드에서 오류가 발생할 가능성이 높아집니다.
// 예시) 🚫 숫자 배열 + false
[1,2,3,4] + false → '1,2,3,4false' // 배열이 사라지고 string으로 바뀜
// 🚫 함수의 인자가 잘못 들어가도 실행됨
function add(a, b) {
  return a + b
}
add(1) → NaN //return값이 NaN일 뿐, 에러가 나지 않음
const a = { a: "A" };
a.hello();
// 선언 되지 않은 함수를 호출할 때: 실행 시 에러 발생(실행 전에 에러 감지 불가)

⭐ Tyescript

  • 타입 안정성과 사전에 에러를 발생시켜 오류(버그)를 감소시킵니다.
  • TypeScript는 JavaScript에 추가적인 구문을 추가하여 editor와의 단단한 통합을 지원합니다. editor에서 초기에 오류를 잡을 수 있습니다.
  • TypeScript 코드는 JavaScript가 실행되는 모든 곳(브라우저, Node.js 또는 Deno 및 앱 등)에서 JavaScript로 변환될 수 있습니다.
  • TypeScript는 JavaScript를 이해하고 타입 추론(type inference)을 사용하여 추가 코드 없이도 훌륭한 도구를 제공합니다.

타입스크립트 코드 테스트

https://www.typescriptlang.org/play

타입스크립트 핸드북

https://typescript-kr.github.io/pages/basic-types.html

타입스크립트 Type 정의

  • ✅ 배열: 자료형[]
  • ✅ 숫자: number
  • ✅ 문자열: string
  • ✅ 논리: boolean
  • ✅ optional

명시적 정의(변수 선언 시 타입 정의)

let a: boolean = "x"
// 🚫 boolean 타입에 string타입 할당 불가 알림

변수만 생성(타입 추론)

let b = "hello"; // b가 string 타입이라고 추론
b = 1 // string으로 선언한 변수에 number값 할당
// 🚫 string 타입에 number타입 할당 불가 알림

Alias(별칭) 타입, optional로 선언

type-aliases

→ 타입값에 ?로 값을 주면 optional

❗ ?를 :앞에 붙이면 optional

// ✅ Alias(별칭) 타입
type Player = {
    name: string,
    age?:number
}

const player : Player = {
    name: "nico"
}

⭐ 함수에서는 어떻게 쓸까
type Player = {
    name: string,
    age?:number
}
// ❌ player.age가 undefined일 가능성 알림
if(player.age < 10) {
}

// ⭕ player.age가 undefined일 가능성 체크
if(player.age && player.age < 10) {
}

함수에서 사용시

function playerMaker1(name:string) : Player {
    return {
        name
    }
}

const playerMaker2 = (name:string) : Player => ({name})

const nico = playerMaker1("nico")
nico.age = 12

readonly

JavaScript에서는 mutability(변경 가능성)이 기본값이지만 타입스크립트에서는 readonly를 통해 읽기 전용으로 만들 수 있습니다.

interface Pizza {
  readonly x: number;
}

let pizza: Pizza = { x: 1 };
pizza.x = 12; // error


const numbers: readonly number[] = [1, 2, 3, 4]
🚫 numbers.push(1)

// ❗ readonly가 있으면 최초 선언 후 수정 불가 ⇒ immutability(불변성) 부여
// but, javascript에서는 그냥 배열

readonly-and-const

Tuple

정해진 개수와 순서에 따라 배열 선언

const player: [string, number, boolean] = ["nico", 1, true]
// ❗ readonly도 사용가능 ⇒ readonly [...] 형태

undefined, null, any

  • any: 아무 타입
  • undefined: 선언X 할당X
  • null: 선언O 할당X

unknown

unknown타입은 모든 값을 나타냅니다. 이것은 any타입과 비슷하지만 any보다 unknown이 더 안전합니다. 이유는 unknown값으로 작업을 수행하는 것은 합법적이지 않기 때문입니다.

// 예시1
let a:unknown

if(typeof a === 'number'){
  let b = a + 1
}
if(typeof a === 'string'){
  let b = a.toUpperCase()
}
🚫 let b = a + 1

// 예시2
function hello(a: any) {
  a.b(); // OK
}

function hello2(a: unknown) {
  a.b(); // 에러: Object is of type 'unknown'.
}

void

void는 값을 반환하지 않는 함수의 반환 값을 나타냅니다. 함수에 return 문이 없거나 해당 return 문에서 명시적 값을 반환하지 않을 때 항상 유추되는 타입입니다.

function hello() {
  console.log('x')
}

function noop() {
  return;
}

never

일부 함수는 값을 반환하지 않습니다. 이는 함수가 예외를 throw하거나 프로그램 실행을 종료함을 의미합니다.

function fail(msg: string): never {
  throw new Error(msg);
}

function temp(name:string|number):never {
  if(typeof name === "string"){
    name
  } else if(typeof name === "number"){
    name
  } else {
    name
  }
}
  • if 안에서는 string형의 name 반환합니다.
  • else if 안에서는 number형의 name 반환합니다.
  • else 안에서는 never형의 name 반환합니다.
  • ⇒ 즉, 제대로 인자가 전달되었다면 else로 올 수 없습니다.

Function

Call Signatures

프로퍼티로 호출 가능한 것을 설명하려면 객체 타입에 Call Signature을 작성할 수 있습니다. Call(=Function) Signature란 함수의 매개변수와 반환 값의 타입을 모두 type으로 미리 선언하는 것을 말하며 Call Signatures는 다음과 같이 함수의 매개 변수(parameter)와 반환 타입을 지정합니다.

type Add = {
  (a: number, b: number): number;
}

const add: Add = (a, b) => a + b

주의

const add:Add = (a,b) => a+b
// 위의 Arrow함수를 일반 함수로 풀면 다음과 같게 됩니다.
function add(a, b) {
  return (a+b)
}

const add:Add = (a,b) => {a+b}
// 위의 Arrow함수를 일반 함수로 풀면 다음과 같게 됩니다.
function add(a, b) {
  a+b;
}

// 애로우함수에서 {}를 사용하게 되면 그 안의 값은 반환이 아니라 함수 내부 내용으로 처리되기에 반환값이 없는 void로 처리됩니다.
// 이에 따라 위에서 미리 선안한 Add자료형의 반환값은 number라고정해놓은 내용과 충돌하기에 에러가 발생합니다.
type PizzaFunction = {
  pizza: string;
  (args: number): boolean;
};

function hello(fn: PizzaFunction) {
  console.log(fn.pizza, fn(6));
}

call-signatures

Overloads

하나의 함수가 복수의 Call Signature를 가질 때 발생합니다. 동일한 이름에 매개 변수와 매개 변수 타입 또는 리턴 타입이 다른 여러 버전의 함수를 만드는 것을 말합니다. TypeScript에서는 오버로드 signatures을 작성하여 "다양한 방식으로 호출할 수 있는 함수"를 지정할 수 있습니다.

type Add={
  (a:number,b:number):number;
  (a:number,b:number,c:number):number;
}

const add:Add=(a, b, c?:number) => {
  if (c) return a + b + c;
  return a + b;
}

add(1,2)
add(1,2,3)

overloads

polymorphism(다형성)

다형성이란, 여러 타입을 받아들임으로써 여러 형태를 가지는 것을 의미합니다. 인자들과 반환값에 대하여 형태(타입)에 따라 그에 상응하는 형태(타입)를 갖을 수 있습니다.

타입스크립트에서 다형성을 가지기 위해선 제너릭을 사용합니다. 제네릭은 C#이나 Java와 같은 언어에서 재사용 가능한 컴포넌트를 만들기 위해 사용하는 기법으로 단일 타입이 아닌 다양한 타입에서 작동할 수 있는 컴포넌트를 생성할 수 있습니다.(구체적인 타입을 지정하지 않고 다양한 인수와 리턴 값에 대한 타입을 처리할 수 있습니다.) 타입스크립트에서 제네릭을 통해 인터페이스, 함수 등의 재사용성을 높일 수 있습니다. 제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이라 정리할 수 있습니다.

재너릭을 사용하지 않았을 때

// 모든 타입을 다 적어주어야 함
type SuperPrint = {
  (arr: number[]): void,
  (arr: string[]): void,
  (arr: boolean[]): void,
  (arr: (number|string|boolean)[]): void
}

const superPrint: SuperPrint = (arr) => {
  arr.forEach(e => console.log(e));
}

superPrint([1, 2, 3])
superPrint(["a", "b", "c"])
superPrint([true, false, true])
superPrint([1, "b", true])

위와 같이 다양한 경우를 커버하는 함수를 작성할 때, 모든 조합의 Call Signature를 concrete type으로 적어주는 일은 번거롭습니다.

재너릭을 사용할 때

type SuperPrint = {
  (arr: T[]): T
}

const superPrint: SuperPrint = (arr) => arr[0]

// (arr: number[]) => number
superPrint([1, 2, 3])

// (arr: string[]) => string
superPrint(["a", "b", "c"])

// (arr: boolean[]) => boolean
superPrint([true, false, true])

// (arr: (string | number | boolean)[]) => string | number | boolean
superPrint([1, "b", true])

위의 코드처럼 type에 Generic을 할당하면 호출된 값으로 concrete type을 가지는 Call Signature를 역으로 보여주는 다형성(Polymorphism)을 가지게 됩니다

함수에서 제너릭을 사용할 때

function identity< Type >(arg: Type): Type {
  return arg;
}

// 제네릭 화살표 함수 (tsx기준)
const identity=< Type extends {} >(arg: Type):Type => {
  return arg;
}

let output = identity< string >("myString"); // 첫 번째 방법
let output = identity("myString"); // 두 번째 방법
// 두 번째 방법은 type argument inference(타입 인수 유추)를 사용합니다. 즉, 컴파일러가 전달하는 인수 유형에 따라 자동으로 Type 값을 설정하기를 원합니다.

any를 넣는 것과 Generic의 차이는 무엇일까요?

// Any
type SuperPrint = {
  (arr: any[]): any
}

const superPrint: SuperPrint = (arr) => arr[0]

let a = superPrint([1, "b", true]);

// pass
a.toUpperCase(); // any를 사용하면 위와 같은 경우에도 에러가 발생하지 않습니다.

// Generics
type SuperPrint = {
  (arr: T[]): T
}

const superPrint: SuperPrint = (arr) => arr[0]

let a = superPrint([1, "b", true]);

// error
a.toUpperCase();
// Generic의 경우 에러가 발생해 보호받을 수 있다
// 이유는 Call Signature를 concrete type으로 하나씩 추가하는 형태이기 때문입니다.

type SuperPrint = {
  (arr: T[], x: M): T
}

const superPrint: SuperPrint = (arr, x) => arr[0]

let a = superPrint([1, "b", true], "hi");

// 위와 같이 복수의 Generic을 선언해 사용할 수도 있습니다

'any'를 사용하는 것은 어떤 타입이든 받을 수 있다는 점에서 'generics'과 같지만 함수를 반환하는데 있어 'any'는 받았던 인수들의 타입을 활용하지 못합니다. 정리하면 any와의 차이점은 해당 타입에 대한 정보를 잃지 않는다는 차이점으로 any는 any로서 밖에 알 수 없지만 generics는 타입 정보를 알 수 있습니다.

Generics

classes And Interfaces

classes

추상(abstract) 클래스

추상 클래스는 오직 다른 클래스가 상속받을 수 있는 클래스로 직접 새로운 인스턴스를 만들 수는 없습니다.

abstract class User {
  constructor(
    private firstname:string,
    private lastname:string,
    public nickname:string
  ){
    abstract getNickname():void
  }
}

class Player extends User {
// 추상 메서드는 추상 클래스를 상속받는 클래스들이 반드시 구현(implement)해야하는 메서드입니다.
  getNickname(){
    console.log(this.nickname)
  }
}

접근 가능한 위치 정리

구분 선언한 클래스 내상속받은 클래스 내인스턴스

private
protected
public

public: 모든 클래스에서 접근 가능 private: 해당 클래스 내에서만 접근 가능 (자식 클래스에서도 접근 불가) protected: 해당 클래스와 자식 클래스에서 접근 가능

예시: 사전 구현하기

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];
  }

  update(word: Word) {
    if (this.words[word.term] !== undefined) {
      this.words[word.term] = word.def;
    }
  }

  del(term: string) {
    if (this.words[term] !== undefined) {
      delete this.words[term];
    }
  }
}

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

const kimchi = new Word("kimchi", "super cool food");
const pizza = new Word("pizza", "super nice piazza");
const dict = new Dict();

dict.add(kimchi);
dict.add(pizza);
console.log("KIMCHI:", dict.def("kimchi"));
console.log("PIZZA:", dict.def("pizza"));

dict.update(new Word("kimchi", "very incredible super food"));
console.log("UPDATE KIMCHI:", dict.def("kimchi"));
console.log("NOT UPDATE PIZZA:", dict.def("pizza"));

dict.del("pizza");
console.log("DELETE PIZZA", dict.def("pizza"));
console.log("NOT DELETE KIMCHI:", dict.def("kimchi"));

classes

Static Members

클래스에는 static 멤버가 있을 수 있습니다. 이 멤버는 클래스의 특정 인스턴스와 연결되지 않습니다. 클래스 생성자 객체 자체를 통해 액세스할 수 있습니다. static 멤버는 동일한 public, protected 및 private 과 함께 사용할 수도 있습니다. static Methods는 클래스를 생성안해도 사용이 가능합니다.

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();

static-members

Interfaces

객체의 모양을 특정해주기 위해 사용합니다. 여기서는 firstName 및 lastName 필드가 있는 객체를 설명하는 인터페이스를 사용합니다.

interface Person {
  firstName: string;
  lastName: string;
}

interfaces

implements

implements을 사용하여 클래스가 특정 인터페이스를 충족하는지 확인할 수 있습니다. 클래스를 올바르게 구현하지 못하면 오류가 발생합니다. implements절은 클래스가 인터페이스 유형으로 처리될 수 있는지 확인하는 것입니다. 클래스의 유형이나 메서드는 전혀 변경하지 않습니다. 또한 클래스는 여러 인터페이스를 구현할 수도 있습니다.

// 클래스 C는 A, B를 구현합니다.
class C implements A, B { }
interface Pingable {
  ping(): void;
}

// Sonar클래스는 Pingable인터페이스를 implement했기 때문에 Pingable가 가진 ping메서드를 구현해줘야 합니다.
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}

implements

Type Aliases과 Interfaces의 차이점

Type Aliases과 인터페이스는 매우 유사하며 많은 경우 자유롭게 선택할 수 있습니다. 인터페이스의 거의 모든 기능은 type에서 사용할 수 있으며, 주요 차이점은 type을 다시 열어 새 속성을 추가할 수 없는 것입니다. 반면 인터페이스는 항상 확장 가능합니다.

differences-between-type-aliases-and-interfaces

@ts-check

JavaScript 파일에서 오류를 활성화하려면 // @ts-check를 .js 파일의 첫 번째 줄에 추가하여 TypeScript가 오류를 발생시키도록 합니다. TypeScript는 �여러 오류를 제공할 수 있습니다. 이러한 오류를 무시하고 싶다면 // @ts-ignore 또는 // @ts-expect-error를 추가하여 특정 줄의 오류를 무시할 수 있습니다. ts-check

JSDoc Reference

JSDoc 주석을 사용하여 JavaScript 파일에 type 정보를 제공할 수 있습니다. (자바스크립트 파일에서 타입 정보를 제공할 수 있습니다.) jsdoc-supported-types

@param, @returns

/**
* @param {string} p1 - A string param.
* @param {string=} p2 - An optional param (Google Closure syntax)
* @param {string} [p3] - Another optional param (JSDoc syntax).
* @param {string} [p4="test"] - An optional param with a default value
* @returns {string} This is the result
*/
function stringsStringStrings(p1, p2, p3, p4) {
  // 코드...
}
반응형