TypeScript 기본 문법 총정리

Donghwa Kim·2022년 10월 10일
0

TypeScript

목록 보기
2/2

본 글은 제로초님의 강의 [리뉴얼] 타입스크립트 올인원 : Part1. 기본 문법편 을 듣고 스스로 정리한 내용임을 밝힙니다.

들어가며


이번 포스팅에서는 TS 기본 문법을 총 정리 해보려고 한다.
자바스크립트로 쓰여진 코드를 타입스크립트로 마이그레이션 하는 것이 목표이다.

따라서, 타입스크립트 동작원리를 현 시점에서 깊게 파고들기 보다는 사용법을 빠르게 익혀 코드에 적용해보려고 한다.

TS 기본 문법


기본적으로 변수, 속성, 매개변수, 리턴값에 타입이 붙었다고 생각하면 됨.

const a: number = 5;
function add(x: number, y: number): number { return x + y }
const add: (x: number, y: number) => number = (x, y) => x + y;
const obj: { lat: number, lon: number } = { lat: 37.5, lon: 127.5 };

특수한 타입 {} (null과 undefined가 아닌 모든 타입)

const z: {} = 5;
const zz: {} = null // Type 'null' is not assignable to type '{}'

type alias (타입 별명), 약속

type Add = (x: number, y: number) => number
const add2: Add = (x, y) => x + y;

ts가 추론해주는 타입이 있는데 이런 건 그냥 그대로 사용하면 됨. ts가 추론하지 못하는 경우에만 직접 타이핑할 것.

  • 아래 코드에서 변수 a의 타입을 number로 지정해주면 type이 number로 변한다. const로 선언한 변수 a는 5라는 값으로 고정이기 때문에 number보다는 숫자 5가 타입으로 좀 더 명확하다고 볼 수 있다.
const a = 5; // const a: 5
const b = '3'; // const b: "3"
const c = a + b; // const c: string
function add(x: number, y: number) { return x + y } // function add(x: number, y: number): number

: 뒷부분, as 뒷부분, <> 부분, interface, type, function 일부를 제외하면 자바스크립트와 동일. 제외하고 생각하는 연습을 초반에 해야 함.

const obj: { lat: number, lon: number } = { lat: 37.5, lon: 127.5 };
const obj = { lat: 37.5, lon: 127.5 };

const a = document.querySelector('#root') as HTMLDivElement;
const a = document.querySelector('#root');

function add<T>(x: T, y: T): T { return x + y }
function add(x, y) { return x + y }

interface A {};
type A = {};

자바스크립트에 비해서 자유도가 확 줄어듦 (ex: 변수에 문자열을 넣었다가 숫자로 바꾸는 등의 행동 어려워짐)

let x = 5;
x = 'hello'; // Type 'string' is not assignable to type 'number'.(2322)

any를 최대한 쓰지 말자

  • any는 어떤 타입의 값도 할당 가능
  • 그러면.. Typescript를 쓰는 의미가 없음

최대한 ! 대신 if를 쓸 것

  • ! (Non-null assertion operator)
  • 피연산자가 Nullish(null이나 undefined) 값이 아님을 단언
  • 결론부터 말하면 안쓰는게 좋다.
  • 어떤 변수에 null 혹은 undefined 값이 들어가지 않음을 완전히 통제하기는 힘들기 때문
  • !를 쓰지 말고 if문을 쓰자
const head = document.querySelector('#head')!; // const head: Element
console.log(head);

const head = document.querySelector('#head'); // const head: Element | null
if (head) {
  console.log(head);
}

string과 String은 다름. 소문자로 하는 것 기억하기.

const a: string = 'hello';
const b: String = 'hell';

배열, 튜플

  • 튜플: 길이가 정해져 있는 배열 (순서도 보장되어야 함)
    • ⚠아래의 예와 같이 길이를 초과하는 값을 push하는 경우에는 ts가 에러를 뱉지 않음에 주의
    • 최대 인덱스보다 큰 인덱스를 지정해 값을 대입하려 하면 에러 반환
let arr: string[] = []; // let arr: string[]
let arr2: Array<string> = []; // let arr2: string[]
function rest(...args: string[]) {}  // function rest(...args: string[]): void

const tuple: [string, number] = ['1', 1]; // const tuple: [string, number]

tuple[2] = 'hello'; // Type '"hello"' is not assignable to type 'undefined'.(2322)
tuple.push('hello'); // 에러가 안남에 주의!!!!

템플릿 리터럴 타입이 존재(유니언 등 사용 가능)

  • 유니언: 여러 타입 중 하나가 될 수 있는 값을 의미. 세로 막대 (|)로 각 타입을 구분
type World = "world" | "hell";

// type Greeting = "hello world"
type Greeting = `hello ${World}`; // type Greeting = "hello world" | "hello hell"

enum, keyof, typeof

  • enum
    • 아무것도 지정하지 않은 경우에는 0부터 숫자를 매김
    • 값을 각각 지정하여 할당할 수도 있음
    • 문자열도 할당 가능
  const enum EDirection {
    Up, // 0
    Down, // 1
    Left, // 2
    Right, // 3
  }
  
  // Using the enum as a parameter
  function walk(dir: EDirection) {}
  walk(EDirection.Left);
  • 객체를 enum처럼 구현
    • as const를 붙이게 되면 키와 값 모두를 고정 타입으로 선언 가능, readonly 객체
  const ODirection = {
    Up: 0,
    Down: 1,
    Left: 2,
    Right: 3,
  } as const;
 
  // It requires an extra line to pull out the keys
  type Direction = typeof ODirection[keyof typeof ODirection]; // ODirection의 value값들만 들고오는 코드
  function run(dir: Direction) {}

  run(ODirection.Right);

union(|)과 intersection(&)

// union: 또는, 둘 중 하나의 속성만 있으면 된다.
type A = { hello: 'world' } | { zero: 'cho' };
const a: A =  { hello: 'world' }; // OK

// intersection: and, 모든 속성이 다 있어야 한다.
type B = { hello: 'world' } & { zero: 'cho' };
const b: B =  { hello: 'world' }; // error
const bb: B = { hello: 'world', zero: 'cho' }; // OK

type alias를 상속처럼 쓰는 예

type Animal = { breath : true };
type Poyuryu = Animal & { breed : true };
type Human = Poyuryu & { think : true };

const john:Human = { think : true, breath : true,  breed : true }; // OK

interface extends(상속)

interface A {
  breath: true
}

interface B extends A {
  breed: true
}

const b: B = { breath: true, breed: true }; //OK

interface끼리는 서로 합쳐짐

  • 같은 이름의 인터페이스를 여러 번 선언할 수 있음 (type alias는 불가능) ->합쳐짐!
  • 이러한 특성 때문에 라이브러리는 타입이 아니라 보통 인터페이스로 만들어져 있음
  • 내가 원하는 대로 커스터마이징 가능!
interface A { a: string }
interface A { b: string }
// const obj1: A = { a: 'hello'}; // error
const obj1: A = { a: 'hello', b: 'haha'}; // OK

type B = { a: string } // Duplicate identifier 'B'.
type B = { b: string } // Duplicate identifier 'B'.
const obj2: B = { a: 'hello', b: 'world' } // error

객체에서 좁은 타입과 넓은 타입

  • 객체는 구체적일수록 더 좁은 타입
  • 좁은 타입 -> 넓은 타입 대입 가능
type A = { name: string };
type B = { age: number};

type AB = A | B;
type C = A & B;

// 타입C가 타입AB 보다 더 구체적임 -> 좁은 타입
// 따라서 더 넓은 타입을 좁은 타입에 대입할 수 없음
const ab: AB = { name: 'john' };
const c: C = ab; // error

// 반대는 가능 (좁은타입 -> 넓은타입)
const c: C = { name : 'john', age : 20 };
const ab: AB = c;

잉여 속성 검사

  • 좁은 타입과 넓은 타입을 검사할 때, 객체 리터럴을 바로 집어 넣으면 타입에 선언된 속성 외에 속성이 있는지 체크한다. 있으면 에러

  • 선언된 속성 외에 속성이 있는 경우 변수로 한 번 빼주고 대입하면 문제 없음

// 잉여 속성 검사에 걸리는 예
type A = { name: string };
type B = { age: number};

type AB = A | B;
type C = A & B; // { name: string, age: number };

const c: C = { name: 'john' , age : 20 , married: false }; // error

// 변수로 빼주면 해결됨!
type A = { name: string };
type B = { age: number};

type AB = A | B;
type C = A & B; // { name: string, age: number };

const obj =  { name: 'john' , age : 20 , married: false }
const c: C = obj ; // OK

Any vs. Unknown

  • 원칙: any를 쓸 바에는 unknown을 쓴다고 알아두자

  • any의 문제점

    • 타입 검사를 포기해버린다.
    • 타입 스크립트를 쓰는 의미가 없어짐
  • unknown

    • 내가 지금 당장은 타입을 모르겠을 때 쓰는 것
    • 나중에 쓸 때 as로 타입을 정의해 줘야 함
    • 타입 검사를 포기한 any와는 다름
  • 코드 예

interface A {
  talk: () => void;
}

const a: A = {
  talk() { return 3; }
}

// any를 쓰는 예
const b: any = a.talk();
b.method(); // 존재하지 않는 타입을 써도 검사 x

// unknown을 쓰는 예
const b: unknown = a.talk();
b.talk(); // Object is of type 'unknown'.(2571)

(b as A).talk(); // OK


// unknown이 나오는 가장 흔한 예는 try ~ catch문
try {
// var error: unknown
} catch(error) {
  //error.message // error
  (error as Error).message // OK
}

타입 가드 (타입 좁히기)

타입 가드란 어떠한 스코프내에서 타입을 체크해주는(좁혀주는) 표현식을 뜻함

  • 코드 예1: type of
function numOrString(a: number | string) {
    // Property 'toFixed' does not exist on type 'string'.(2339)
    a.toFixed(1);
}

function numOrString(a: number | string) {
  if (typeof a === 'number') {
    // 이 스코프 안에 들어온 매개변수 a의 타입이 number임을 보장
    a.toFixed(1);
  } else {
    // 이 스코프 안에 들어온 매개변수 a의 타입이 string임을 보장
    a.charAt(3);
  }

}

numOrString('123');
numOrString(1);
  • 코드 예2: Array.isArray()
function numOrNumArray(a: number | number[]) {
  if (Array.isArray(a)) { // Array.isArray() 사용
    a.concat(4);
  } else {
    a.toFixed(3);
  }

}

numOrNumArray([1, 2, 3]);
numOrNumArray(1);
  • 코드 예3: {속성} instanceof {클래스}
class A {
  aaa() {}
}
class B {
  bbb() {}
}

// 클래스 자체를 타입자리에 쓸 수 있다.
// 실제로 인자로 들어가는 것은 개체
function aOrB(param: A | B) {
  if (param instanceof A) {
    param.aaa();
  }
}

aOrB(new A());
aOrB(new B());
  • 코드 예4: 객체- 속성으로 구별
type B = { type: 'b', bbb: string};
type C = { type: 'c', ccc: string};
type D = { type: 'd', ddd: string};

// 값으로 구별하는 방법
function typeCheck(a: B | C | D){
  if (a.type === 'b') {
    a.bbb;
  } else if (a.type ==='c') {
    a.ccc;
  } else {
    a.ddd;
  }
}

// 속성명(메서드명 포함)으로 구별하는 방법
function typeCheck(a: B | C | D){
  if ('bbb' in a) {
    a.type;
  } else if ('ccc' in a) {
    a.ccc;
  } else {
    a.ddd;
  }
}
  • 보통 객체의 경우, 값으로 구별하는 방법을 많이 사용

    • 그래서 객체를 만들때는 타입가드를 위해 아래 코드 예처럼 라벨을 달아두는 것이 좋다 (편하다!)
const human = { type: 'human' };
const dog = { type: 'dog' };
const cat = { type: 'cat' };

커스텀 타입 가드

  • 타입 판별 함수를 스스로 만들 수 있음
  • is 활용
    • (주의) is를 써 줘야만 if문안에 쓸 수 있다.
interface Cat { meow: number }
interface Dog { bow: number }

function catOrDog(a: Cat | Dog): a is Dog {

  // 타입 판별을 직접 만들어야 함
  if ((a as Cat).meow) { return false }
  return true;
}

const cat: Cat | Dog = { meow: 3}
if (catOrDog(cat)) {
  console.log(cat.meow);
}

if ('meow' in cat) {
  console.log(cat.meow);
}

{}와 Object

  • {}, Object는 모든 타입! (null과 undefined는 제외)
const x: {} = 'hello';
// const xx: {} = undefined; // error
// const xxx: {} = null; // error

const xxxx: {} = 1234;
const y: Object = 'hi';

const xx: object = 'hi'; // error
const yy: object = { hello: 'world' }; // 실제로 객체 타이핑을 할때는 object 지양. interface, type, class 사용할 것
const z: unknown = 'hi';

// unknown = {} | null | undefined
if (z) {
  z; // const z: {}
} else {
  z; // const z: unknown
}

인덱스드 시그니쳐

객체의 속성이 너무 많을 때, 모든 속성과 값의 타입을 지정해주는 방법

type A = {[key: string]: number};
const a: A = { a: 1, b: 2, };
const aa: A = { a: 'a', b: 'b' } // 값이 number가 아니므로 error

맵드 타입

  • 자바스크립트의 map함수를 타입에 적용한 것
  • 기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 문법
  • 이 맵드 타입을 통해 타입을 제한할 수 있음
type B = 'Human' | 'Mammal' | 'Animal';
type A = { [key in B]: number}; // A 타입의 속성값에는 B의 타입들이 와야 함
const a:A = { Animal: 3, Human: 2, Mammal: 0 };

type AA = { [key in B]: B}; // 값에도 B의 값 중 하나가 와야 함
const aa: AA = {Animal: "Mammal", Human: "Human", Mammal: "Animal"};

클래스

  • 클래스의 원래 모습(?): constructor가 필요함
class A {
  a: string;
  b: number;

  constructor(a: string, b: number = 123) {
    this.a = a;
    this.b = b;
  }

  method() {

  }
}

const a = new A('123'); // { a: "123", b: 123 }
  • constructor 없이 바로 초기값 대입 가능
class A {
  a: string = '10';
  b: number = 3;

  method() {

  }
}

const a: A = new A(); // { a: "10", b: 3 }
  • 클래스 이름이 타입이 될 수 있다고 했음
    • 이 때, 클래스 이름은 인스턴스 (new OO)를 가리킴
  • 클래스 자체를 가리키는 것은 typeof OO
class A {
  a: string;
  b: number;

  constructor(a: string, b: number = 123) {
    this.a = a;
    this.b = b;
  }

  method() {

  }
}

const a: A = new A('123');

const aa: typeof A = new A('45'); // error
const b: typeof A = A; // OK

interface

  • interface를 통해서 class의 모양을 통제할 수 있다.
interface A {
  readonly a: string;
  b: string;
}

class B implements A {
  readonly a: string = '123';
  b: string = 'world';
}

접근제한자 - public, private, protected

  • public
    • 기본이 public, 접근제한자 생략시 public
  • private
    • 클래스 외부에서 접근 불가
    • 클래스 내부에서 접근 가능
    • 상속 받은 클래스에서도 접근 불가
  • protected
    • 클래스 외부에서 접근 불가
    • 클래스 내부에서 접근 가능
    • 상속 받은 클래스에서 접근 가능
class B {
  private a: string = '123';
  protected b: string = 'world';
  public c: string = 'wow';
}

class C extends B {
  method() {
    console.log(this.a); // error
    console.log(this.b); // ok
    console.log(this.c); // ok
  }
}

옵셔널

  • 기본
function abc(a: number, b?: number, c?: number) {}

abc(1);
abc(1, 2);
abc(1, 2, 3);
abc(1, 2, 3, 4); // error
  • 매개변수의 개수가 정해져 있지 않을 때
function abc(...args: number[]) {}

abc(); //OK
abc(1); //OK
abc(1, 2, 3, 4, 5); //OK
abc('1', 2, 3); // error
  • 객체의 속성에서의 옵셔널
let obj: { a: string, b?: string } = { a: 'hello', b: 'world' }
let obj2: { a: string, b?: string } = { a: 'hello',  } // OK

제네릭

  • 함수를 선언할 때는 그 타입을 모르지만 사용할 때 타입이 정해짐
  • extends로 타입에 제한을 둘 수 있음
  • 사용 예
  function add<T extends number | string>(x: T, y: T): T {
    return x + y;
  }

  add(1, 2); // 3
  add('1', '2') // '12'

  add('1', 2); // error
  add('2', 1); // error
  • 두 개이상 사용 가능
  function add<T extends number, Y extends string>(x: T, y: Y): T {
    return x + y;
  }

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

  add(1, '2'); // OK
  • 제네릭의 다양한 예
  function add<T extends { a: string }>(x: T): T { return x }; 
  add({ a: 'hello' });

  function add<T extends string[]>(x: T): T { return x }; 
  add([ 'a', 'b', 'c' ]);

  function add<T extends (a: string) => number>(x: T): T { return x }; 
  add((a)=>+a)

  // 모든 함수
  function add<T extends (...args: any) => any>(x: T): T { return x }; 

  // 생성자
  function add <T extends abstract new (...args: any) => any>(x: T): T { return x };
profile
Slow but steady wins the race🏃‍♂️

0개의 댓글