2장. 타입
타입이란
-
자료형으로서의 타입
- 7가지 데이터 타입(자료형) : undefined, null, Boolean, String, Symbol, Numeric, Object)
- 메모리에 값을 저장 시, 타입을 사용해 값의 종류를 명시해 메모리를 효율적으로 사용 가능
-
집합으로서의 타입
- 값이 가질 수 있는 유효한 범위의 집합
- 코드에서 사용되는 유효한 값의 범위를 제한 → 런타임에 발생할 수 있는 유효하지 않은 값에 대한 에러 방지
-
정적 타입과 동적 타입
-
정적 타입 : 모든 변수의 타입이 컴파일 타임에 결정 → 컴파일 타임에 타입 에러를 발견할 수 있음 → 안정성 보장
-
동적 타입 : 변수 타임이 런타임에서 결정 → 언제 프로그램에 오류가 발생할 지 불안함
-
타입스크립트는 정적 타입이다.
컴파일 타임 : 기계(컴퓨터, 엔진)가 소스코드를 이해할 수 있도록 기계어로 변환되는 시점
런타임 : 변환된 파일이 메모리에 적재되어 실행되는 시점
-
-
강타입과 약타입
-
암묵적 타입 변환 여부에 따라 타입 시스템을 강타입과 약타입으로 분류
암묵적 타입 변환 : 개발자가 의도적으로 타입을 명시하거나 바꾸지 않았는데도 컴파일러 또는 엔진 등에 의해 런타임에 타입이 자동으로 변경되는 것
-
강타입 : 파이썬, 루비, 타입스크립트
-
약타입 : C++, 자바, 자바스크립트
-
서로 다른 타입을 갖는 값끼리 연산을 시도하면
→ 강타입 : 에러 발생
→ 약타입 : 내부적으로 판단해 특정 값의 타입을 변환해 연산을 수행한 후 값을 도출 (암묵적 변환)
-
암묵적 변환 → 편리하지만 예기치 못한 오류가 발생할 가능성도 높아짐
⇒ 타입을 명시하거나 타입스크립트가 타입을 추론하도록 해, 유효하지 않은 작업 수행을 방지해야 함.
-
-
컴파일 방식
- 타입스크립트가 탄생한 이유 : 자바스크립트의 컴파일 타임에 런타임 에러를 사전에 잡아내기 위한 것
- 타입스크립트를 컴파일하면 타입이 모두 제거된 자바스크립트 소스코드만이 남게 됨
타입스크립트의 타입 시스템
-
타입 애너테이션 방식
타입 애너테이션(type annotation) : 변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언해서 어떤 타입 값이 저장될 것인지를 컴파일러에 직접 알려주는 문법
- 변수 이름 뒤에 : type 구문을 붙여 데이터 타입을 명시해줌
-
구조적 타이핑
- 타입스크립트는 구조로 타입을 구분한다. 이것을 구조적 타이핑이라고 한다.
-
구조적 서브 타이핑
구조적 서브 타이핑 : 객체가 가지고 있는 속성(프로퍼티)을 바탕으로 타입을 구분하는 것
- 이름이 다른 객체라도 가진 속성이 동일하다면 타입스크립트는 서로 호환이 가능한 동일한 타입으로 여김
// 예시 1. 가진 속성이 동일한 두 객체 interface Pet { name: string } interface Cat { name: string age: number } let pet: Pet; let cat: Cat = { name: "Zag", age: 2 }; // ✅ OK pet = cat; // 예시 2. 함수의 매개변수에도 적용 interface Pet { name: string } let cat = { name: "Zag", age: 2 }; function greet(pet: Pet) { console.log(`Hello, ${pet.name}`); } greet(cat); // ✅ OK
- 타입의 상속 역시 구조적 타이핑 기반이다
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } class Developer { name: string; age: number; sleepTime: number; constructor(name: string, age: number, sleepTime: number) { this.name = name; this.age = age; this.sleepTime = sleepTime; } } function greet(p: Person) { console.log(`Hello, I'm ${p.name}`); } const developer = new Developer("zig", 20, 7); greet(developer); // Hello, I'm zig
- 서로 다른 두 타입 간의 호환성은 오로지 타입 내부의 구조에 의해 결정된다.
- 타입 A가 타입 B의 서브 타입이라면 A 타입의 인스턴스는 B 타입이 필요한 곳에 언제든지 위치할 수 있다.
- 즉, 타입이 계층 구조로부터 자유롭다.
-
자바스크립트를 닮은 타입스크립트
-
타입스크립트의 타입 시스템은 구조적 타이핑을 사용
-
이는 ‘명목적 타이핑’과는 대조적인 방식
명목적 타이핑 : 타입의 구조가 아닌 타입의 이름만을 가지고 구별하는 것, 같은 이름의 데이터 타입으로 선언된 경우에만 서로 호환됨
-
명목적 타이핑은 타입의 동일성을 확인하는 과정에서 구조적 타이핑에 비해 조금 . 더안전하다.
-
그럼에도 타입스크립트가 구조적 타이핑을 채택한 이유는 타입스크립트가 자바스크립트를 모델링한 언어이기 때문
-
자바스크립트는 본질적으로 덕 타이핑(duck typing)을 기반으로 한다.
덕 타이핑 : 어떤 타입에 부합하는 변수와 메서드를 가질 경우 해당 타입에 속하는 것으로 간주하는 방식.
‘만약 어떤 새가 오리처럼 걷고, 헤엄치며 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다’
- 구조적 타이핑 덕분에 타입스크립트에서는 객체 간 속성이 동일하다면 서로 호환되는 구조적 타입 시스템을 제공하여 편리성을 높임
- 자바스크립트의 덕 타이핑과 타입스크립트의 구조적 타이핑은 서로 구분되는 타이핑 방식이지만, 실제 사용하는 코드를 보면 별 차이가 없어 보인다.
- 두 가지 타이핑 방식 모두 객체가 가진 속성을 기반으로 타입을 검사하기 때문
- 덕 타이핑과 구조적 타이핑 모두 객체 변수, 메서드 같은 필드를 기반으로 타입을 검사한다는 점에서 동일하지만, 타입을 검사하는 시점이 다르다.
- 덕 타이핑은 런타임에 타입을 검사한다. ⇒ 동적 타이핑에서 사용
- 구조적 타이핑은 컴파일 타임에 타입 체커가 타입을 검사한다. ⇒ 정적 타이핑에서 사용
-
-
구조적 타이핑의 결과
- 타입스크립트 구조적 타이핑의 특징 때문에 예기치 못한 결과가 나올 때도 있다.
interface Cube { width: number height: number depth: number } function addLines(c: Cube) { let total = 0; for (const axis of Object.keys(c)) { // 🚨 Element implicitly has an 'any' type // because expression of type 'string' can't be used to index type 'Cube'. // 🚨 No index signature with a parameter of type 'string' // was found on type 'Cube' const length = c[axis]; total += length; } } // 아래와 같은 상황(c[axis]의 타입이 string 일 수 있음) 때문에 에러 발생 const namedCube = { width: 6, height: 5, depth: 4, name: "SweetCube", // string 타입의 추가 속성이 정의되었다 }; addLines(namedCube); // ✅ OK
- 타입스크립트 구조적 타이핑의 특징으로 Cube 타입 값이 들어갈 곳에 name 같은 추가 속성을 가진 객체도 할당할 수 있기 때문에 발생하는 문제
⇒ 이러한 한계를 극복하고자 타입스크립트에 명목적 타이핑 언어의 특징을 가미한 식별할 수 있는 유니온 같은 방법이 생겨났다.
-
타입스크립트의 점진적 타입 확인
점진적 타입 검사 : 컴파일 타임에 타입을 검사하면서 필요에 따라 타입 선언 생략을 허용하는 방식
-
타입 선언을 생략하면 암시적 타입 변환이 일어남
-
타입을 명시하지 않았을 때 타입스크립트 컴파일러는 함수의 인자와 반환 값을 모두 any 타입으로 추론
any 타입 : 타입스크립트 내 모든 타입의 종류를 포함하는 가장 상위 타입으로 어떤 값이든 할당 가능
-
-
자바스크립트 슈퍼셋으로서의 타입스크립트
-
값 vs 타입
값 : 프로그램이 처리하기 위해 메모리에 저장하는 모든 데이터
- 타입을 명시하는 방법으로는
:type
, type 키워드, interface 키워드 등이 있음 - 값 공간과 타입 공간의 이름은 서로 충돌하지 않음 ( ← type으로 선언한 내용은 런타임에서 제거되기 때문)
- 타입은 주로 타입선언(:) 또는 단언문(as)로 작성하고 값은 할당 연산자인 =으로 작성
- 타입과 값이 혼용되는 것 말고도 타입 공간에 동시에 존재하는 심볼도 있음. 대표적인 것이 클래스와 enum
- 클래스 - 타입 애너테이션으로 사용할 수 있지만 런타임에서 객체로 변환되어 자바스크립트의 값으로 사용되는 특징을 가짐
class Developer { name: string; domain: string; constructor(name: string, domain: string) { this.name = name; this.domain = domain; } } const me: Developer = new Developer("zig", "frontend");
- enum - 타입 공간에서 타입을 제한하는 역할을 하지만 자바스크립트 런타임에서 실제 값으로도 사용될 수 있음
enum WeekDays { MON = "Mon", TUES = "Tues", WEDNES = "Wednes", THURS = "Thurs", FRI = "Fri", } // ‘MON’ | ‘TUES’ | ‘WEDNES’ | ‘THURS’ | ‘FRI’ type WeekDaysKey = keyof typeof WeekDays; function printDay(key: WeekDaysKey, message: string) { const day = WeekDays[key]; if (day <= WeekDays.WEDNES) { console.log(`It’s still ${day}day, ${message}`); } } printDay("TUES", "wanna go home");
- 자바스크립트 문법의 여러 키워드가 타입스크립트에서 값 또는 타입으로 해석되는 방식
키워드 값 타입 class Y Y const, let, var Y N enum Y Y function Y N interface N Y type N Y namespace Y N - 타입을 명시하는 방법으로는
enum과 유니온
- 배달이팀 의견
제한은 없지만 enum을 선호하는 편
유니온 타입 → 내가 어떤 타입을 가졌는지 전부 기억해야 하고, 변경이 필요하면 사용되는 곳을 모두 찾아서 변경해야 함 ⇒ 리팩터링하기 번거로움
enum → 정의부를 변경하면 자동으로 변경됨 ⇒ 넓은 범위에 확장해서 사용할 경우 유용, 코드 가독성 좋음
물론, enum은 트리쉐이킹이 되지 않기 때문에 번들 사이즈에 영향을 줄 수 있지만 const enum을 사용하여 해결할 수 있음
트리쉐이킹 : 코드 어디에서도 사용하지 않는 코드(dead code, 이른바 죽은 코드)를 삭제해서 최종 번들 크기를 줄이는 과정
- 사실 enum을 쓴다고 해서 전체 파일의 번들 사이즈가 서비스에 영향을 미칠정도로 커지지는 않음
- 냥이팀 의견
- enum을 잘 사용하지 않음
- enum은 타입을 위한 문법이라기보다 개발을 위한 문법 같음
- enum의 기능이 타입스크립트 컴파일러에 의해서 동작하는 것이 이상하게 느껴짐
- 메이팀 의견
enum을 지양하고 있음
사용하기에는 편하지만 성능에 영향을 줄 수 있다는 걸 본 후로 습관적으로 안 쓰기 시작 enum 외에 const enum을 사용하나요?
- 배달이팀
- enumaration(열거) 폴더를 따로 만들어서 사용하고 있음
- 이 폴더에 정의한 enum을 외부에서 전역적으로 참조할 때는 const enum을 사용함
- const enum은 빌드 과정에서 참조 값만 남기기 때문에 트리쉐이킹이 된다는 장점이 있음
- 어차피 enum도 상수를 쓰기 위한 것으로 생각하기 때문에 const enum을 사용하는 것이 적절하다고 판단됨
- 물론 isolate 모드를 활성화하고 const enum을 쓰면 안 된다는 의견도 있음
- 현재로서는 const enum이 더 명확하고 정적인 값을 사용할 수 있다는 장점이 더 큰 것 같음
- 왕팀
const enum을 사용하지 않음
const enum은 enum과 다르게 직접적인 값으로 치환되기 때문에 전체 네임스페이스에 접근하지 못하고 순회할 수 없다는 단점을 가지고 있는 것 같음 트리쉐이킹
- 자바스크립트, 타입스크립트에서 사용하지 않는 코드를 삭제하는 방식
- 웹팩, 롤업 같은 모듈 번들러를 사용해 번들링 작업을 수행할 때 사용하지 않는 코드는 자동으로 삭제
- 트리쉐이킹을 통해 더 작은 크기의 번들링 파일을 생성할 수 있음
-
타입을 확인하는 방법
- typeof, instanceof, 타입 단언을 사용해서 확인 가능
- typeof : 연산하기 전에 피연산자의 데이터 타입을 나타내는 문자열 반환
class Developer { name: string; sleepingTime: number; constructor(name: string, sleepingTime: number) { this.name = name; this.sleepingTime = sleepingTime; } } const d = typeof Developer; // 값이 ‘function’ type T = typeof Developer; // 타입이 typeof Developer // type T에 할당된 Developer는 인스턴스의 타입이 아니라 new 키워드를 사용할 때 볼 수있는 생성자 함수 const zig: Developer = new Developer("zig", 7); type ZigType = typeof zig; // 타입이 Developer // zig 인스턴스는 Developer가 인스턴스 타입으로 생성됨
- typeof 연산자처럼 instanceof 연산자의 필터링으로 타입이 보장된 상태에서 안전하게 값의 타입을 정제하여 사용할 수 있음
let error: unknown; if (error instanceof Error) { showAlertModal(error.message); } else { throw Error(error); }
- 타입 단언으로 타입을 강제할 수 있는데 이때 as 키워드를 사용하면 됨
- 타입 단언은 강제 형 변환과 유사한 기능을 제공
const loaded_text: unknown; // 어딘가에서 unknown 타입 값을 전달받았다고 가정 const validateInputText = (text: string) => { if (text.length < 10) return "최소 10글자 이상 입력해야 합니다."; return "정상 입력된 값입니다."; }; validateInputText(loaded_text as string); // as 키워드를 사용해서 string으로 강제하지 않으면 타입스크립트 컴파일러 단계에서 에러 발생
- 타입을 검사하는 방법으로 ‘타입 가드’라는 패턴도 있음
- 타입 가드 : 특정 조건을 검사해서 타입을 정제하고 타입 안정성을 높이는 패턴
-
원시 타입
- 자바스크립트의 변수 → 어떤 타입의 값이라도 자유롭게 할당 가능
- 타입스크립트 → 이 변수에 타입을 지정할 수 있는 타입 시스템 체계를 구축
- 원시 타입 종류 : boolean, undefined, null, number, bigInt, string, symbol
-
객체 타입
-
객체 타입 종류 : object, , array, type과 interface 키워드, function
-
object
- 가급적 사용하지 말도록 권장
- any 타입과 유사하게 객체에 해당하는 모든 타입 값을 유동적으로 할당할 수 있어 정적 타이핑의 의미가 크게 퇴색되기 때문
-
(중괄호)
- 객체를 타이핑할 때 사용하는데, 중괄호 안에 객체의 속성 타입을 정해주는 식으로 사용
const obj:{} = {};
와 같이 사용도 가능. 그러나 이 경우엔 어떠한 값도 속성으로 할당 불가능
-
array
- 타입스크립트 배열 타입은 하나의 타입 값만 가질 수 있다는 점에서 자바스크립트 배열보다 조금 더 엄격
- 타입스크립트에서 배열을 선언하는 방식은 Array 키워드 또는 대괄호([])를 사용
- 주의할 것은 ‘튜플’ 타입도 대괄호로 선언한다!
- 튜플은 배열과 유사하지만 튜플의 대괄호 내부에는 선언 시점에 지정해준 타입 값만 할당할 수 있음.
- 원소 개수도 타입 선언 시점에 미리 정해줌
- 이는 객체 리터럴에서 선언하지 않은 속성을 할당하거나, 선언한 속성을 할당하지 않을 때 에러가 발생한다는 점에서 비슷
const targetCodes: ["CATEGORY", "EXHIBITION"] = ["CATEGORY", "EXHIBITION"]; // (O) const targetCodes: ["CATEGORY", "EXHIBITION"] = [ "CATEGORY", "EXHIBITION", "SALE", ]; // (X) SALE은 지정할 수 없음
-
type과 interface 키워드
-
중괄호를 사용한 객체 리터럴 방식으로 타입을 매번 일일이 지정하기에는 중복적인 요소가 많음
-
type 또는 interface 키워드를 사용해 선언하면 반복적으로 사용돼도 중복 없이 해당 타입 사용 가능
-
type과 interface를 둘 다 쓸 수 있는 상황에서 팀은 주로 어떤 것을 사용하나요?
-
배달이팀
- 대부분의 상황에서 interface 사용, 간단한 용도로는 type도 사용
- 공식 문서에 쓰인 내용을 바탕으로 전역적으로 사용할 때는 interface를, 작은 범위 내에서 한정적으로 사용한다면 type
-
냥이팀
- type은 어떤 값에 대한 정의같이 정적으로 결정되어 있는 것을,
- interface는 확장될 수 있는 basis를 정의하거나 어떤 object 구성을 설명하는 요소
-
봉다리팀
- 선언 병합이 필요할 때는 interface
- computed value를 사용해야 한다면 type
-
왕팀
- interface의 필요성을 느끼지 못하고 있어 type 정의를 주로 사용
팀 내에서 type이나 interface만을 써야 하는 상황이 있었나요?
- 배달이팀
- interface : 객체 지향적으로 코드를 짤 때, 특히 상속하는 경우에 (ex: extends, implements 사용 시)
- 냥이팀
- type : 유니온 타입이나 교차 타입 등 type 정의에서만 쓸 수 있는 기능을 활용할 때
- interface : 기존 인터페이스를 정의하고 확장할 때
- 메이팀
- type : props에 Record 형식을 extends할 때 interface로 선언된 변수를 넣으면 에러가 발생해서 type으로 변경함. interface처럼 인덱스 키가 따로 설정되지 않으면 오류가 발생하는 듯
- 봉다리팀
- type : computed value를 써야 했을 때
-
function
-
함수 자체의 타입을 지정하는 방식은 ‘호출 시그니처’를 정의하는 방식
호출 시그니처 : 함수의 매개변수와 반환 값의 타입을 명시하는 역할을 함
type add = (a: number, b: number) => number;
- 타입스크립트에서 함수 자체의 타입을 명시할 때는 화살표 함수 방식으로만 호출 시그니처를 정의
-
-