Typescript: 타입스크립트 프로그래밍

Lumpen·2024년 6월 1일
0

Typescript

목록 보기
18/23

타입 계층

type 은 프로그램이 가지는 데이터의 종류이면서 할 수 있는 작업의 집합이라고 볼 수 있다
객체 타입 어노테이션을 작성할 떄에는 가장 좁은 범위로 해야 한다
다형성을 갖기 때문에 상위 타입으로 어노테이션으로 작성하여도 오류가 발생하지 않지만
컴파일 타임에 해당 객체가 갖지 않는 상위 객체의 메소드 등을 호출하였을 경우 검사하지 못할 수 있다

타입 별칭

특정 타입을 변수처럼 새로운 이름으로 담아 사용할 수 있다

type a = number

a 라는 이름으로 number 타입 사용

타입 연산

Union (|)

합집합으로 A 또는 B 또는 둘 모두의 프로퍼티를 가진다

Intersection (&)

교집합으로 A 와 B 의 모든 프로퍼티를 가진다

없음을 나타내는 값

  • null: 값의 부재를 나타내는 값의 타입
  • undefined: 아직 정의되지 않은 값의 타입
  • void: 아무 것도 반환하지 않는 함수의 반환 타입
  • never: 절대 반환하지 않는 함수 타입 (무한루프 등)

unknown 은 모든 타입의 상위 타입이라면 never 는 모든 타입의 서브 타입이다
컴파일 타임에 null 을 검출할 수 있는 것이 좋은 타입 시스템이라고 할 수 있다

열거형 (enum)

열거형은 해당 타입으로 사용할 수 있는 값을 열거하는 것
키를 값에 할당하는 순서가 없는 자료 구조

enum Language1 {
    English,
    Korean
}

enum Language2 {
    English = 0,
    Korean = 1
}

위와 같이 타입스크립트가 적절한 숫자를 추론하게 할 수도 있지만
아래와 같이 명시적으로 값을 지정할 수도 있다
추론 시에는 이전 숫자의 다음 정수로 추론하거나

타입스크립트는 값이나 키로 열거형에 접근할 수 있도록 허용하지만
불안정한 결과를 준다
가지지 않는 키에 대해 컴파일 타임에 접근을 하지 못하도록 막아줘야 하지만
enum 에 대해서는 그렇지 않다
const enum 을 사용하면 이러한 부분에 대해 안전성을 확보할 수 있다

const enum

const enum Language {
    English,
    Korean
}

const enum 은 역방향 찾기를 지원하지 않아 자바스크립트 객체와 비슷해진다
또한 일반 enum 처럼 자바스크립트 코드를 생성하지 않는다
대신 필요한 곳에 열거형 멤버의 값을 채워 넣는다
버전에 따라 열거형 값을 다르게 채워넣을 수도 있기 때문에 되도록 열거형 자체를 멀리해야 한다
하지만 문자열 값을 갖는 const 열거형을 사용하면 안전하게 사용할 수 있다

let, var 에 대해서는 타입스크립트가 일반적인 타입으로 추론하지만
const 를 사용하면 구체적 타입을 추론하게 할 수 있다

함수

나머지 매개변수

인수의 개수가 고정된 개수를 갖는게 아니라면 나머지 매개변수를 사용할 수 있다

this

클래스 메서드를 제외한 모든 곳에서 this 를 사용하지 않는 방향으로

문맥적 타입화

함수 타입 작성 시 매개변수와 반환 타입을 지정하였다면
함수 작성 시 따로 다시 작성할 필요는 없다
이런 것을 문맥적 타입화라고 한다

오버로드

함수 오버로드 시 중괄호로 작성

type CreateElement = {
    (tag: ‘a’): void
    (tag: string): string
}

다형성

구체 타입

기대하는 타입을 알고 있을 때 유효하다
기본 타입과 객체 타입 모두 구체 타입이라고 할 수 있다

제네릭 타입

제네릭 타입 매개변수는 여러 장수에 타입 수준의 제한을 줄 때 사용하는
플레이스홀더 타입이다
filter 함수에 대해 제네릭을 적용하면

type Filter = {
	<T>(array: T[], f: (item: T) => boolean): T[]
}

타입 스크립트가 T 에 대해 추론하게 하는 것이다
T 는 자리를 맡아둔다는 의미의 플레이스 홀더 타입이다
타입 검사기는 문맥을 보고 T 를 실제 타입으로 채운다

제네릭은 구체 타입을 사용할 때 보다 더 함수의 기능을 일반화 할 수 있다
가능하면 제네릭을 사용하여 코드를 일반화하여 재사용성을 높이자

제네릭 타입의 타입 한정 시기

제네릭 타입은 선언 위치에 따라 타입의 범위와 타입 한정 시기가 결정된다

type Filter = {
	<T>(array: T[], f: (item: T) => boolean): T[]
}

type Filter<T> = {
	(array: T[], f: (item: T) => boolean): T[]
}

type Filter = <T>(array: T[], f: (item: T) => boolean): T[]

type Filter<T> = (array: T[], f: (item: T) => boolean): T[]

function filter<T>(array: T[], f: (item: T) => boolean): T[] {}
  1. T 의 범위를 개별 시그니처로 한정한 전체 호출 시그니처로 해당하는 개별 함수를 호출할 때 T 의 구체 타입을 한정한다
  2. T 의 범위를 모든 시그니처로 한정한 전체 호출 시그니처로 함수를 선언할 때 T 의 구체타입을 한정한다
  3. 호출 시 T 의 구체 타입을 한정한다
  4. 선언 시 T 의 구체 타입을 한정한다
  5. 호출 시 T 의 구체 타입을 한정한다

제네릭 타입 추론

대부분의 상황에서 타입스크립트는 제네릭 타입을 추론해낸다
제네릭도 명시적으로 지정할 수 있다
제네릭 타입을 명시할 때는 모든 제네릭 타입을 명시하거나
아무 것도 하지 않는다

제네릭 타입 별칭

타입 별칭 시 타입 별칭 명과 할당 기호 (=) 사이에만 제네릭 타입을 선언할 수 있다

한정된 다형성

<T extends A>
T 는 A 이거나 A 의 하위 타입이다
하위 타입이라는 것은 A 를 상속받았다는 뜻으로 기능은 확장된다

T 라고만 하면 타입의 상한이 사라지고
A 라고만 하면 타입이 축소되어 확장된 하위 타입들이 무시된다

여러개의 타입 제한

여러개의 타입을 제한하려면 인터섹션(&)을 사용한다

<T extends A & B>

가변 인수 함수 정의하기

가변 인수 함수를 정의할 때 한정된 다형성을 사용할 수 있다

<T extends unknown[]>
T 는 어떤 타입의 배열 또는 튜플이다

function call<T extends unknown[], R>(
	f: (...args: T) => R,
    ...args: T
): R {
	return f(...args)
}

제네릭 타입 기본값

함수 매개변수에 기본값을 설정하듯
제네릭 타입 매개변수에도 기본 타입을 지정할 수 있다

<A, T = HTMLElement>
기본 타입을 갖는 제네릭은 가장 마지막에 작성해야 한다
특정 요소의 타입을 제대로 알 수 없을 때를 대비해서 작성한다

타입 주도 개발

타입 시그니처를 정하고 값을 나중에 채우는 프로그래밍 방식
표현력이 풍부한 타입 시스템일수록 표현식 안의 값을 더 잘 설명할 수 있다
표현력이 높은 타입 시스템을 함수에 적용하면
함수 타입 시그니처를 통해 함수에 관하는 거의 모든 정보를 알 수 있다

function map<T, U>(array: T[], f: (item: T) => U): U[] {
	
}

map 을 본적 없어도 이 시그니처를 보고 map 이 어떤 동작을 하는지 알 수 있다

함수 구현 시 먼저 함수의 타입 시그니처를 정의한 후 구현을 한다
구현 전 타입 수준에서 구상할 때 함수가 이치에 맞는지 상위 수준에서 확인할 수 있다

클래스와 상속

접근 한정자

  • public: 어디에서나 접근 가능
  • protected: 해당 클래스와 서브 클래스 인스턴스에서만 접근 가능
  • private: 해당 클래스에서만 접근할 수 있다

접근 한정자를 이용해 내부 구현 정보를 제한할 수 있다

추상 클래스

abstract 키워드를 이용하여 타입 시스템이 해당 클래스를 상속받은 클래스를 통해서만
인스턴스화할 수 있도록 허용할 수 있다

absctract class Piece {
	constructor ()
}

Piece 클래스를 직접 인스턴스화하려고 시도하면 에러가 발생한다

new Piece() // error TS2511: 추상 클래스의 인스턴스는 생성할 수 없음

absctract 키워드는 해당 클래스를 바로 인스턴스화 할 수 없음을 의미할 뿐 아니라
필요한 메서드를 추상 클래스에 자유롭게 추가할 수 있다
추상 클래스에는 메서드와 추상 메서드를 추가할 수 있다

absctract class Piece {
	constructor ()
  	moveTo(position: Position) {
    	this.position = position
    }
	abstract canMoveTo (position: Position): boolean
}

Piece 클래스를 상속 받을 경우
추상 메서드인 canMoveTo 메서드를 구현하지 않으면 컴파일 타임에 에러가 발생한다
추상 메서드는 반드시 구현해야 한다 (추상 클래스의 추상 메서드는 선언부만 작성)
메서드인 moveTo 는 기본 구현을 포함한 상태로 상속되며 필요 시 오버라이드할 수 있다

  • class 키워드로 클래스 선언 후 extends 키워드로 상속 받을 수 있다
  • 클래스는 구체 클래스와 추상 클래스로 분류된다. 추상 클래스는 추상 메서드와 추상 프로퍼티를 가질 수 있다
  • 메서드는 접근 한정자를 가질 수 있고 기본 값은 public
  • 클래스는 인스턴스 프로퍼티도 가질 수 있으며 프로퍼티도 접근 한정자를 가질 수 있다
  • 인스턴스 프로퍼티를 선언할 때 readonly 를 추가할 수 있다

super

자식 클래스가 부모 클래스에 정의된 메서드를 오버라이드 하면
자식 인스턴스는 super 를 이용해 부모 버전의 메서드를 호출할 수 있다
타입 스크립트는 다음과 같이 두 가지의 super 호출을 지원한다

  • super.take 같은 메서드 호출
  • 생성자 함수에서만 호출할 수 있는 super()
    자식 클래스에 생성자 함수가 있다면 super() 를 호출해야 부모 클래스와 정상적으로 연결된다

super() 로 부모 클래스의 메서드에 접근할 수 있지만 프로퍼티는 접근할 수 없다

this 를 반환 타입으로 사용하기

this 는 값 뿐 아니라 타입으로도 사용할 수 있다
클래스를 정의할 때라면 메서드의 반환 타입을 지정할 때 this 타입을 유용하게 활용할 수 있다

class Set {
	add(value: number): Set
}

add 를 호출하면 Set 인스턴스를 반환하는 클래스가 있을 때
Set 을 상속받는 MutableSet 서브클래스가 있다면

class MutableSet extends Set {
	add(value: number): MutableSet
}

MutableSet 을 반환하도록 오버라이드 해야 한다
이렇게 자기 자신을 반환하는 클래스를 상속할 경우
매번 add() 를 오버라이드 해야 한다

이럴 때 this 를 반환 타입으로 사용할 수 있다

class Set {
	add(value: number): this
}

Set 을 상속 받는 서브클래스는 이제 자기 자신을 반환하게 된다

인터페이스

클래스는 인터페이스를 통해 사용할 때가 많다
타입 별칭처럼 인터페이스도 타입에 이름을 지어주는 수단으로
타입을 더 깔끔하게 정의할 수 있다
기능적으로 타입 별칭과 거의 같지만
타입은 연산이 가능하고
인터페이스는 확장이 가능하다

type Food = a: string

type Sushi = Food & {
	b: string
}
interface Food {
	a: string
}

interface Sushi extends Food {
	b: string
}

인터페이스가 반드시 다른 인터페이스를 상속 받아야 하는 것은 아니다
객체 타입, 클래스, 인터페이스 모두 상속 받을 수 있다

선언 합침

선언 합침은 같은 이름으로 정의된 여러 정의를 자동으로 합치는 타입스크립트의 기능이다
interface 는 같은 이름으로 정의하면 하나의 인터페이스로 합쳐진다
하지만 타입 선언은 중복되면 안된다

구현

클래스를 선언할 때 implements 키워드를 이용해 특정 인터페이스를 만족시킴을 표현할 수 있다
implements 로 타입 수준의 제한을 추가하면 구현에 문제가 있을 때 어디가 문제되었는지 쉽게 파악할 수 있다
어댑터, 팩토리, 전략 등 디자인 패턴을 구현하는 대표적인 방식이기도 하다

interface Animal {
	eat(): void
  	sleep(): void
}

class Cat implements Animal {
	eat() {
    	console.log('eat')
    }
  	sleep() {
    	console.log('zzz')
    }
}

Cat 은 Animal 에 선언된 모든 메서드를 구현해야 하며
필요하다면 메서드나 프로퍼티를 추가할 수 있다
implements 는 인터페이스 구현이다

인터페이스로 인스턴스 프로퍼티를 정의할 수 있지만
접근 제한자(private, protected, public)를 선언할 수 없으며
static 키워드도 사용할 수 없다
readonly 는 설정할 수 있다

한 클래스가 하나의 인터페이스만 구현할 수 있는 것은 아니다
필요하면 여러 인터페이스를 구현할 수 있다

추상클래스 vs 인터페이스

  • 추상클래스: 클래스인데 추상메서드를 가지고 있는 클래스
  • 인터페이스: 추상메서드만 가지고 있고 그 외에는 아무것도 가지고 있지 않음

추상클래스와 인터페이스의 차이점은
추상클래스는 인스턴스 변수를 가질 수 있지만
인터페이스는 인스턴스 변수를 가질 수 없다

인터페이스가 더 범용으로 쓰이며 가벼운 반면
추상 클래스는 특별한 목적과 기능을 갖는다는 점이 다르다
인터페이스는 형태를 정의하는 것으로 객체, 배열, 함수, 클래스, 클래스 인스턴스를 정의할 수 있다
자바스크립트 코드를 만들지 않고 컴파일 타임에만 존재한다
추상 클래스는 클래스만 정의할 수 있고 런타임 자바스크립트 코드를 만든다
생성자와 기본 구현을 가질 수 있으며 접근 제한자를 지정할 수 있다

클래스는 구조 기반 타입

타입스크립트는 클래스를 비교할 때
다른 타입과 달리 이름이 아닌 구조를 기준으로 삼는다
클래스는 자신과 똑같은 프로퍼티와 메서드를 정의하는 기존의 일반 객체를 포함해
클래스의 형태를 공유하는 다른 모든 타입과 호환된다
타입스크립트에서는 Zebra 를 인수로 받는 함수에 Poodle 을 전달한다고 에러를 발생시키지는 않는다
단 private 나 protected 필드를 갖는 클래스는 다르다
private 나 protected 필드가 있고,
할당하려는 클래스나 서브클래스의 인스턴스가 아니라면 할당할 수 없다고 판정한다

class A {
	private x = 1
}
class B extends A {}
function f(a: A) {}

f(new A) // OK
f(new B) // OK

f({ a: A}) // { x: number } 타입은 매개변수 'A' 타입에 할당할수 없음
// { x: number }  는 private 가 아님

클래스는 값과 타입을 모두 선언한다

타입스크립트의 거의 모든 것은 값 아니면 타입이다
값과 타입은 타입스크립트에서 별도의 네임스페이스에 존재한다
문맥을 보고 타입스크립트가 알아서 값 또는 타입으로 해석한다

클래스와 열거형은
네임스페이스에 타입을, 값 네임스페이스에 값 동시에 생성한다

클래스와 열거형은 타입 수준에서 타입을 생성하기 때문에
is-a 관계를 쉽게 표현할 수 있다
타입스크립트는 구조 기반으로 타입을 지정하기 떄문에
특정 클래스가 정의한 것과 형태가 같은 모든 객체를
그 타입에 할당할 수 있다

런타임에 new 로 클래스를 인스턴스화 하거나
클래스의 정적 메서드를 호출, 메타 프로그래밍, intanceof 연산 등을 하려면 클래스의 값이 필요하다

typeof 키워드를 사용하면 클래스 자체를 가리킬 수 있다
-> 자바스크립트에서는 값 수준의 typeof 가 있고 타입스크립트에서는 타입 수준의 typeof 가 있다
new 코드는 생성자 시그니처라 부른다
생성자 시그니처는 new 연산자로 해당 타입을
인스턴스화할 수 있음을 정의하는 타입스크립트 방식이다
타입스크립트는 구조 기반으로 타입을 구분하기 떄문에
이 방식이 클래스가 무엇인지 기술하는 최선이다

클래스 정의는 용어를 값 수준과 타입 수준으로 생성할 뿐 아니라
타입 수준에서 두 개의 용어를 생성했다
하나는 클래스의 인스턴스를 가리키고
하나는 (typeof 연산자로 얻을 수 있는) 클래스 생성자 자체를 가리킨다

다형성

함수와 타입처럼 클래스와 인터페이스도 기본값과 상한/하한 설정을 포함한
다양한 제네릭 타입 매개변수 기능을 지원한다
제네릭 타입의 범위는 클래스나 인터페이스 전체가 되게 할 수 있고
특정 메서드로 한정할 수도 있다

함수와 마찬가지로 제네릭에 구체 타입을 명시하거나
타입스크립트가 타입을 추론하도록 할 수 있다

믹스인

자바스크립트와 타입스크립트는 trait 이나 mixin 키워드를 제공하지 않지만
직접 구현할 수 있다

두 키워드 모두 둘 이상의 클래스를 상속 받는 다중 상속과 관련된 기능을 제공한다
역할 지향 프로그래밍에서는 Shape 으로 표현하는 대신
측정할 수 있다, 네 개의 면을 갖고 있다 등으로 속성을 묘사한다

is-a 대신 can, has-a 관계를 사용한다

믹스인이란 동작과 프로퍼티를 클래스로 혼합할 수 있게 해주는 패턴으로

  • 상태를 가질 수 있다
  • 구체 메서드만 제공할 수 있다
  • 생성자를 가질 수 있다

믹스인은 클래스 생성자를 인수로 받아 클래스 생성자를 반환하는 함수
믹스인은 동작을 캡슐화할뿐만 아니라 동작을 재사용할 수 있도록 도와준다

final 클래스

타입스크립트는 final 키워드를 지원하지 않지만 비슷하게 만들 수 있다
생성자를 private 로 선언하면 new 로 인스턴스를 생성하거나 클래스를 확장할 수 없다
클래스 상속만 막아야 하지만 비공개 생성자를 사용하면 인스턴스화 기능도 같이 사라진다
이럴 때 static create() 메서드 등의 인스턴스 생성 메서드를 따로 만들어
final 키워드와 같은 동작을 할 수 있다

디자인 패턴

팩토리 패턴, 빌더 패턴, 등

고급 타입

타입스크립트는 하스켈 프로그래머의 부러움을 살 정도로 강력한 타입 수준 프로그래밍 기능을 갖춘 타입 시스템을 가지고 있다

서브타입, 슈퍼타입

서브타입

두 개의 타입 A 와 B 가 있고
B 가 A 의 서브타입이면 
A 가 필요한 곳에는 어디든 B 를 안전하게 사용할 수 있다

슈퍼타입

두 개의 타입 A 와 B 가 있고 
B 가 A 의 슈퍼타입이면 
B 가 필요한 곳에는 어디든 A 를 안전하게 사용할 수 있다

가변성

매개변수화된(제네릭) 타입 등 복합 타입에서는 복잡하다
복합 타입의 서브타입 규칙은 프로그래밍 언어마다 다르다

형태와 배열 가변성

type ExistingUser = {
	id: number
  	name: string
}

function deleteUser(user: { id?: number, name: string }) {
	delete user.id
}

deleteUser(ExistingUser)

타입스크립트에서 위의 경우 객체의 프로퍼티를 삭제하더라도 타입스크립트에서는 인식하지 못한다
ExistingUser 의 타입이 deleteUser 함수 user 인수 타입의 서브 타입이기 때문에
타입스크립트는 완벽함보다는 실수를 잡는 것과 쉬운 사용 두 가지 목표를 균형 있게 달성하는 것이 목표다
파괴적 갱신은 실무에서 비교적 드물게 일어나므로 적극 제지하지 않고
슈퍼타입이 필요한 곳에 객체를 할당할 수 있도록 허용한다

type LegacyUser = {
	id?: number | string
  	name: string
}

const legacyUser: LagacyUser ={
	id: '793331',
  	name: 'Xin Yang'
}

function deleteUser(user: { id?: number, name: string }) {
	delete user.id
}

deleteUser(lagacyUser) 
// error TS2345: 'LegacyUser' 인수 타입은 '{id?: number | undefined, name: string }' 타입에 할당할 수 없음
// 'string' 타입은 'number | undefined' 타입에 할당할 수 없음

기대하는 타입의 슈퍼타입 프로퍼티를 포함하는 형태를 전달하면 타입스크립트는 에러를 발생시킨다
슈퍼타입의 id 는 string | number | undefined 인데
deleteUser 는 id 가 number | undefined 인 상황만 처리할 수 있기 때문이다

어떤 형태를 요구할 때 건넬 수 있는 타입은
요구되는 타입에 포함된 프로퍼티 각각에 대해 '<:기대하는 타입'인 프로퍼티들을 가지고 있어야 한다
기대하는 프로퍼티 타입의 슈퍼타입이 있다면 건넬 수 없다
타입스크립트의 타입은 그들의 프로퍼티 타입에 공변한다고 말한다
객체 B 에 할당할 수 있는 객체 A 가 있다면 객체 A 의 각 프로퍼티 <: B 의 대응 프로퍼티라는 조건을 만족해야 한다

가변성 정리

타입스크립트에서 모든 복합 타입의 멤버(객체, 클래스, 배열, 함수, 반환타입)는 공변이며
함수 매개변수 타입만 반변이다

함수 가변성

typescript strict 모드에서 함수의 매개변수는 반공변성을 띈다
strict 모드가 아니라면 함수의 매개변수는 양변성을 띈다
함수의 반환 타입은 공변성을 띈다

함수 A 가 함수 B 와 같거나 적은 수의 매개변수를 가지며 다음을 만족하면 A 는 B 의 서브타입이다

  1. A 의 this 타입을 따로 지정하지 않으면 'A 의 this 타입 >: B 의 this 타입'이다
    -> A this 가 B this 의 서브타입
  2. 'A 의 각 매개변수 >: B 의 대응 매개변수'이다
    -> A 매개변수 가 B 매개변수 의 서브타입
  3. 'A 의 반환타입 <: B 의 반환타입'이다
    -> A 는 B 의 서브타입이므로 A 는 B 의 반환 타입에 할당 할 수 없지만
    B 는 A 에 할당할 수 있다
    (A 는 B 의 기능을 포함하고 있지만 B 는 A 의 기능이 포함되어 있지 않다)
profile
떠돌이 생활을 하는. 실업자, 부랑 생활을 하는

0개의 댓글