JS 런타임은 코드가 실행될 때 자신이 무엇을 해야할지 결정하기 위해 값의 타입, 즉 해당 값이 어떤 동작과 능력을 가지고 있는지 확인한다. string이나 number와 같은 원시형 데이터는 typeof 연산자를 써서 각 값의 타입을 실행전에 알 수 있으나, 함수가 인자로 오는 경우엔 코드 실행 전에 타입을 예측할 수 없다. 즉, 자바스크립트는 동적 타입만 제공한다. 그래서 순수 자바스크립트에선 argument가 2개 필요한 함수의 파라미터로 1개만 넣어도 실행 전까지는 오류가 나지 않는다. 그렇게 많은 runtime error를 마주치게 되고, 이에 대한 대안으로 등장한 것이 정적 타입 시스템을 사용해 코드 실행 전에 코드를 예측하는 것이다. 코드 실행 전에 에러를 잡고 우리가 사용할 수 있는 프로퍼티를 제안하고자 TS가 등장하게 되었다.

Strongly-typed

타입스크립트를 설치하면 타입스크립트의 컴파일러인 tsc도 함께 설치된다. 타입스크립트 코드를 작성하면 자바스크립트 코드로 컴파일하는데, 이때 타입 에러가 있으면 아예 자바스크립트로 컴파일되지 않는다. tsc --target 명령어로 타입스크립트 코드를 어떤 ECMAScript 버전으로 변환할지 지정할 수 있다.

타입스크립트 설정으로 noImplicitAny, 타입이 암묵적으로 any로 추론되는 변수(묵시적 타입)에 대해 오류를 발생시키게 하거나 strictNullChecksnull이나 undefined를 명시적으로 처리하도록 제한할 수도 있다.

타입을 any로 지정하면 TS의 모든 보호장치가 해제된다고 보면 되고 readonly로 지정하면 말 그대로 데이터 읽기만 가능해진다. (이 때 readonly 여부를 JS로 변환하진 않는다.) 타입을 unknown으로 설정하면 어떤 작업 전 이 변수의 타입을 먼저 체크하도록 하도록 강제한다. 변수의 타입을 미리 알지 못할 때 사용한다.

const player: readonly [string, number, boolean] = ['hail', 1, true];
player[0] = 'hi' // TSError: readonly입니다.

let a: unknown;
let b = a+1; // TSError: 타입 명시되지 않음

if (typeof a==='number') {
	let b= a+1;   // 이 구역에서는 a의 타입이 number
}

그래서 위 코드에서는 readonly 데이터인 player[0]에 값을 지정하려고 해 타입스크립트 에러가 발생한다.

사용법

type alias로 아래처럼 타입에 이름을 지정해주자.

type Age = number;
type Player = {
	age?: Age
}

Call Signature로 함수 인자와 리턴값의 타입을 알려주자.

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

void는 함수가 아무것도 리턴하지 않을 때, never는 함수가 절대 리턴하지 않을 때, 즉 exception이 발생할 때나 절대 실행될 일이 없을 때의 타입이다.

function hello(): never {
	return 'x'  // TSError: 이 함수는 리턴값이 없어야 한다.
}

function hello(): never {
	throw new Error('x') // O
}

function hello(name: string| number) {
	if (typeof name==='string') {
	} else if (typeof name ==='number') {
	} else {
		name // 절대 실행될 일 없다
	}
}

오버로딩을 해 함수가 서로다른 여러개의 콜 시그니처를 가지게 할 수도 있다.

type Push = {
(path: string): void
    (config: Config): void
}

const push:  Push = (config) => {
    if(typeof config === 'string') {
        console.log(config)
    } else {
        console.log(config.path)
    }
}

Type narrowing

function foo(x: string | number) {
  if (typeof x === 'string') {
    // 이 부분에서 x의 타입은 string일 수밖에 없다. (narrowed type)
    x = 1; // 그러나 이런식으로 할당을 하면 선언한 타입이 된다.
  }
  
  // 또는
  if (Array.isArray(x)) {
      // 
  }
}

이렇게 제어 흐름에 따라 타입이 좁혀지는 것을 type narrowing이라고 한다. Type narrowing을 해서 코드 구조를 바탕으로 TS가 더 구체적인 타입 추론을 할 수 있게 해야한다.

다형성 적용하기

Generic

type SuperPrint = {
	<TypePlaceholder>(arr: TypePlaceholder[]): void
}

type SuperPrint = <T>(a: T[]) => T;

// a
const superPrint: SuperPrint = (a) => a[0]

// b
function superPrint<V>(a: V[]) {
    return a[0]
}

위 코드의 a와 b 코드는 생김새만 다를 뿐 동일하다.
<> 기호로 확정된 타입 대산 placeholder를 지정할 수 있다. 이렇게 제너릭을 쓰면 타입스크립트가 placeholder로 타입을 알아내 그 자리를 대체해준다. 코드를 자바스크립트로 변환할 때 타입스크립트가 placeholder 자리의 타입을 적절한 타입으로 변환한다.

type Player<E> = {
    name: string
    extraInfo: E
}

const nico: Player<{age: number}> = {
    name: 'nico',
    extraInfo: {
        age: 29
    }
}

위 코드처럼 사용할 수도 있다.

다형성 타입 집합을 정의할 때는 Union 활용

// as-is
type Animal = {
  kind: 'bird' | 'fish';
  isSwimming?: boolean;
  isFlying?: boolean;
}

// to-be
type Bird = {kind: 'bird', isSwimming: boolean};
type Fish = {kind: 'fish', isFlying: boolean};
type Animal = Bird | Fish;

위 예시의 Animalkindbird인 데이터를 위한 isSwimmingfish 데이터를 위한 isFlying 속성을 함께 가진다. 이런 경우엔 Bird 타입과 Fish 타입으로 분리하고 Union을 사용해 Animal 타입으로 묶어주는 게 낫다. 이 때 Union 타입의 조합에 사용된 각 타입(bird, fish)을 '멤버'라고 한다.

여기서 더 개선하고 싶다면, 아래대로 isBirdisFish 함수가 타입명제를 반환하도록 변경하고 filter를 사용하면 된다.

function isBird(animal: Animal): animal is Bird {
  return animal.kind === 'bird';
}

function isFish(animal: Animal): animal is Fish {
  return animal.kind === 'Fish';
}

const birds = animals.filter(isBird);

interface

타입스크립트의 interface는 객체지향 프로그래밍의 개념을 활용해 디자인되었다. 자바스크립트에서 추상클래스는 상속받는 클래스의 청사진이다. 상속받는 클래스가 가져야할 프로퍼티, 메서드를 알려준다. (상속받는 클래스에게 어떻게 구현할지 말고 뭘 구현해야할지를 알려준다.) 추상클래스는 인스턴스를 만드는 걸 허용하지 않는다. 아래 코드에서 extends User는 할 수 있으나 new User()는 할 수 없다.

타입스크립트에서 추상클래스를 만들면 JS로 변환시 그냥 일반 클래스로 변환한다. 그렇기에 추상클래스 대신 인터페이스를 쓰면 좋다. 더욱이, interface는 컴파일하면 자바스크립트로 바뀌지 않고 사라지므로 가볍다!

// js. 추상클래스
abstract class User { 
	constructor(
		protected firstName: string,
		protected lastName: string
	){}
	
	abstract sayHi(name: string): string;
	abstract getFullName(): string;
}

class Player extends User {
	getFullName() {
		return `${this.lastName}${this.firstName}`
	}
	sayHi(name: string) {
		return `Hello ${name}, I'm ${this.firstName}`
	}
}

// ts. interface를 사용하자
interface User {
	firstName: string
	lastName: string
	sayHi(name: string): string
	getFullName(): string
}

객체의 모양을 선언하는 목적으로 쓰는데, type은 정해진 문자열 값이라던가 특정한 값을 타입으로 제한 가능하지만 interfacetype만 타입으로 정해줄 수 있다.

type은 새 프로퍼티를 추가하도록 개방될 수 없지만 interface는 타입을 상속 받아 프로퍼티를 확장할 수 있다. 아래 코드를 보자. 다만 interface를 상속할때에는 프로퍼티를 private으로 만들 수 없다.

type User = { 
	name: string	
}

type Player = User & {
    // a
}

// User를 interface로 하면
interface Player extends User {
    // b
}