

Typescript 톺아보기
회사에서 새로운 프로젝트를 개발할 때 Back은 Springboot로 개발하고, Front를 NextJS로 개발한다고 한다.
블로그 만들 때 타입스크립트 깔짝대며 찍먹을 했는데 이번엔 푹먹이 필요한 상황이다
공식문서 천천히 읽으며 부족한 부분은 여기보며 부족한 부분을 채워보자.
타입스크립트를 이해한다는 말은 타입스크립트가 어떤 기준으로 타입을 정의하고, 어떤 기준으로 타입들간의 관계를 정의하고, 어떤 기준으로 타입스크립트 코드의 오류를 검사하는지 그 원리와 동작 방색을 낯낯이 살펴본다는 말이다.
1. 시작하기
const obj = {width: 10, height: 15};
// 오타
const area = obj.width * obj.heigth;
console.log(area);
// Nan
x = 1000;
if(1 < x < 3) {
console.log(1);
// true
}
대부분의 프로그래밍 언어는 이런 종류의 오류들이 발생하면 오류를 표출해주고, 일부는 컴파일 중에 오류를 표출해준다.
하지만 자바스크립트는 언어 상 이러한 문제가 많이 발생하고, 이런 문제 때문에 버그가 발생 시 원인을 찾기 어려운 이슈로 이어진다.
정적 타입 검사
- 프로그램을 실행시키지 않으면서 코드의 오류를 검출하는 것을 정적 검사라고 한다.
- 정적 타입 검사자인 TypeScript는 프로그램을 실행시키기 전에 값의 종류를 기반으로 프로그램의 오류를 찾는다.
타입이 있는 Javascript 상위 집합
- Typescript는 JS구문이 허용되는 상위 집합언어이다
- 독특한 구문 때문에 JavaScript코드를 오류로 보지 않는다.
- JavaScript 파일의 코드를 TypeScript 코드로 옮기면, 코드를 어떻게 작성했는지에 따라 타입 오류를 볼 수 있다.
런타임 특성
- Javascript의 런타임 특성을 가진 프로그래밍 언어이다.
- Javascript에서 0으로 나누는 행동은 런타임예외로 처리하지 않고 Infinity값을 반환한다.
- 논리적으로, TypeScript는 JavaScript 코드의 런타임 특성을 절대 변화시키지 않는다.
- TypeScript가 코드에 타입 오류가 있음을 검출해도, JavaScript 코드를 TypeScript로 이동시키는 것은 같은 방식으로 실행시킬 것을 보장한다.
삭제된 타입
- TypeScript의 컴파일러가 코드 검사를 마치면 타입을 삭제해서 결과적으로 컴파일된 코드를 만든다.
- 결론적으로 컴파일 도중에는 타입 오류가 표출될 수 있지만, 타입 시스템 자체는 프로그램이 실행될 때 작동하는 방식과 관련이 없다.
1.0 타입스크립트 동작 원리
- AST(추상 문법 트리) : 코드의 공백이나 주석 탭 등의 코드 실행에 관계없는 그런 요소들을 전부 제거하고 트리 형태의 자료구조에 코드를 쪼개서 저장해 놓은 형태
- 타입과 관련된 코드들은 컴파일 결과 모두 사라진다.
1.1 컴파일러 옵션
- 타입스크립트의 컴파일은 우리가 작성한 코드에 타입 오류가 없는지 검사하고 오류가 없다면 자바스크립트 코드로 변환한다.
- 타입 오류를 검사할 건지, 자바스크립트 코드의 버전은 어떻게 할 것인지 등 컴파일의 세부적인 사항을 컴파일러 옵션이라 한다.
- 컴파일 옵션은 패키지 루트 폴더 아래에 tsconfig.json이라는 파일에 설정할 수 있으며 Node.js 패키지 단위로 설정된다.
tsc를 이용해 tsconfig.json 컴파일러 옵션 파일을 자동 생성할 수 있다.
tsc --init
include옵션
- tsc에게 컴파일 할 타입스크립트 파일의 범위와 위치를 알려주는 옵션
{
"include": ["src"]
}
이렇게 설정하면 이제 tsc명령어만 입력해도 src 폴더 아래의 모든 타입스크립트 파일이 동시에 컴파일 된다.
target옵션
컴파일 결과 생성되는 자바스크립트 코드의 버전을 설정하는 target 옵션
{
"compilerOptions": {
"target": "ES5"
},
"include": ["src"]
}
tsc를 이용해 컴파일하고 결과를 확인하면 es5문법으로 변환된다.
module옵션
tsconfig.json에 module옵션을 추가하고 값으로 CommonJS를 설정한다.
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS" // ESNext -> ES 모듈 시스템
},
"include": ["src"]
}
import, export를 require나 exports등의 CommonJS문법으로 코드가 변환된다.
outDir
컴파일 결과 생성할 자바스크립트 코드의 위치를 결정하는 outDir옵션
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"outDir": "dist"
},
"include": ["src"]
}
tsc를 이용해 컴파일하면 컴파일 결과가 dist폴더에 생성된다.
{
"compilerOptions": {
...
"strict": true
},
"include": ["src"]
}
이 옵션을 true로 설정하면 이제 코드를 아주 엄격하게 검사하게 된다.
ModuleDetection 옵션
- 타입스크립트의 모든 파일은 기본적으로 전역 파일(모듈)로 취급된다.
// a.ts
const a = 1; // ❌
// b.ts
const a = 1; // ❌
이럴 때 각 파일에 모듈 시스템 키워드(export, import)를 최소 하나 이상 사용해 해당 파일을 전역 모듈이 아닌 로컬모듈로 취급되도록 만들어야 하는데 이를 자동화하는 옵션이 바로 moduleDetection옵션이다.
다음과 같이 moduleDection옵션을 force로 설정 할 경우 자동으로 모든 타입스크립트 파일이 로컬 모듈(독립 모듈)로 취급된다.
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"outDir": "dist",
"moduleDetection": "force"
},
"include": ["src"]
}
1.2 타입은 집합이다
타입스크립트의 ‘타입’은 사실 여러개의 값을 포함하는 ‘집합’이다.
집합은 동일한 속성을 갖는 여러개의 요소들을 하나의 그룹으로 묶은 단위를 말한다.
하나의 값만 포함하는 NumberLiteral타입은 딱 하나의 값만 포함하는 아주 작은 집합이다.
모든 Number Literal 타입은 Number 타입이라는 거대한 집합에 포함되는 부분 집합으로 볼 수 있다.
타입스크립트의 모든 타입들은 집합으로 서로를 포함하고 또 포함되는 관계를 갖는다. Number 타입처럼 다른 타입을 포함하는 타입을 슈퍼타입이라고 부르고, 반대는 서브타입이라고 한다.
타입계층도는 여러가지 기본 타입들간의 집합으로 부모 - 자식 관계이다.
타입 호환성
타입 호환성이란 A타입을 B타입으로 취급해도 괜찮은지 판단하는 것을 의미한다.
타입스크립트에서는 이렇게 슈퍼타입의 값을 서브타입의 값으로 취급하는것을 허용하지 않습니다. 반대로는 허용합니다.
객체 타입의 호환성
- 객체또한 업캐스팅은 허용하고 다운 캐스팅은 허용하지 않는다.
type Animal = {
name: string;
color: string;
};
type Dog = {
name: string;
color: string;
breed: string;
};
let animal: Animal = {
name: "기린",
color: "yellow",
};
let dog: Dog = {
name: "돌돌이",
color: "brown",
breed: "진도",
};
animal = dog; // ✅ OK
dog = animal; // ❌ NO
언뜻 보면 Dog 타입이 더 많은 프로퍼티를 정의하고 있어 슈퍼타입처럼 보일 수 있지만 그 반대이다.
type Book = {
name: string;
price: number;
};
type ProgrammingBook = {
name: string;
price: number;
skill: string;
};
let book2: Book = { // ❌ 오류 발생
name: "한 입 크기로 잘라먹는 리액트",
price: 33000,
skill: "reactjs",
};
func({ // 오류 발생
name: "한 입 크기로 잘라먹는 리액트",
price: 33000,
skill: "reactjs",
});
let book3: Book = programmingBook; // 앞서 만들어둔 변수
func(programmingBook);
‘초과 프로퍼티 검사’ 때문에 에러 발생
단순히 변수를 초기화할 땐 에러 발생하지 않음
2. 예제를 실행할 환경 설정
- npm download
npm install -g typescript
- npm init, tsx설치
npm init
npm i @types/node
npm install --save-dev tsx
npm install --save-dev typescript
-
ts_server.ts파일 생성
-
npm script 추가
"tss" : "npx tsx ts_server.ts"
3. 핸드북
3.1 기본 타입
기본 타입(Basic Types)이란?
- 타입스크립트가 자체적으로 제공하는 타입
- 다른말로는 내장 타입이라고 할 수 있다.
원시 타입이란?
- 동시에 한개의 값만 저장할 수 있는 타입
변수 이름 뒤에 콜론(:)과 함께 변수의 타입을 정의하는 이런 문법을 ‘타입 주석’ 또는 **‘타입 어노테이션’**이라 부른다.
- Boolean
let isDone: boolean = false;
- 숫자
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
- 문자열
let fullName: String = `Bob Bobbington`;
let sentence: String = `Hello, my name is ${fullName}.`
- string타입은 문자열을 의미하는 타입
- 단순 쌍따옴표 문자열 뿐만 아니라 작은 따옴표, 백틱, 템플릿 리터럴로 만든 모든 문자열을 포함합니다.
- 배열
- 자바스크립트의 배열과 크게 다르지 않다.
- 배열 타입은 두가지 방법으로 쓸 수 있다.
- 첫번째 방법 - 타입뒤에
[]작성 - 두번째 방법 - 제네릭 배열 타입 사용
- 첫번째 방법 - 타입뒤에
let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
let doubleArr : number[][] = [
[1, 2, 3],
[4, 5],
];
여러 타입의 배열 요소를 가질 경우 아래와 같이 사용한다.
let multiArr: (number | string)[] = [1, "hello"];
여러 타입 중 하나를 만족하는 타입을 정의하는 문법을 유니온 타입이라 부르는데 아래서 더 자세히 살펴본다.
- 튜플
- 타입스크립트에서만 특별히 제공되는 타입
- 요소 타입과 개수가 고정된 배열 표현이 가능
- 튜플을 선언하고 초기화한 후에는 배열처럼 인덱스로만 접근 가능
- 튜플은 결국 배열 -> 그러므로 배열 메서드인 push나 pop을 이용해 고정된 길이를 무시하고 요소를 추가하거나 삭제할 수 있다.
let tuple: [string, number];
tuple = ["hello", 10];
// 아래의 경우 사용
const users: [string, number][] = [
["이정환", 1],
["이아무개", 2],
["김아무개", 3],
["박아무개", 4],
[5, "조아무개"], // 오류 발생
];
- 열거
- 열거형 타입은 자바스크립트에는 존재하지 않고 오직 타입스크립트에서만 사용할 수 있는 특별한 타입
- 값의 집합에 더 나은 이름을 붙여줄 수 있다.
- 모든 값을 수동으로도 설정이 가능
- enum은 컴파일 결과 객체가 된다.
enum Color {RED = 0, GREEN = 1, BLUE = 2}
enum Color {RED, GREEN, BLUE} // 할당하지 않도록 0부터 1씩 늘어나는 값으로 자동 할당
let c : Color = Color.GREEN
let cString : string = Color[1];
console.log(c); // 1
console.log(cString); // GREEN
enum Color2 {RED = 1, GREEN, BLUE}
let c2 : Color2 = Color2.GREEN
console.log(c2); // 2
// 문자열 열거형
enum Language {
korean = "ko",
english = "en",
}
- Any
- 알지 못하는 타입을 표현할 때
- 타입 검사를 하지 않을 때
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // 성공, 분명히 부울입니다.
- Void
- 어떤 타입도 존재할 수 없음을 나타낸다
- any의 반대타입
- 보통 함수에서 반환 값이 없을 때 반환타입을 표현하기 위해 사용
function warnUser(): void {
console.log("asdf");
}
- Null and Undefined
- 기본적으로 null과 undefined는 다른 모든 타입의 하위 타입이다.
- null과 undefined를
number같은 타입에 할당할 수 있다. - 하지만, –strictNullChecks를 사용하면, null과 undefined는 오직 any와 각자 자신들 타입에만 할당 가능합니다. (예외적으로 undefined는 void에 할당 가능합니다)
undefined은 변수를 선언하고 값을 할당하지 않은 상태이다.null은 변수를 선언하고 빈 값을 할당한 상태(빈 객체)이다.
null값을 다른 타입의 변수에 할당하기
null값을 변수의 임시값으로 활용하고 싶을 때 tsconfig.json의 strictNullChecks옵션을 false로 설정한다.
기본값은 true이다.
{
"compilerOptions": {
...
"strictNullChecks": false,
...
},
...
}
- Never
- 절대 발생할 수 없는 타입을 나타낸다.
- 함수 표현식이나 화살표 함수 표현식에서 항상 오류를 발생시키거나 절대 반환하지 않는 반환타입으로 쓰인다
never타입은 모든 타입에 할당 가능한 하위 타입이다.- 어떤 타입도
never에 할당할 수 있거나 하위 타입이 아니다. - 심지어
any도never에 할당할 수 없다. - 의도적으로 오류를 발생시키는 함수도 never탕비으로 반환값 타입을 정의
객체 (Object)
- object는 원시 타입이 아닌 타입을 나타낸다.
- number, string, boolean, bigint, symbol, null, 또는 undefined 가 아닌 나머지를 의미
let user: object = {
id: 1,
name: "이정환",
};
// 접근 불가
user.id;
let user: {
id: number;
name: string;
} = {
id: 1,
name: "이정환",
};
// 접근 가능
user.id
객체 리터럴 타입
중괄호를 열고 객체가 갖는 프로퍼티를 직접 나열해 만드는 타입
객체의 타입을 정의할 때 프로퍼티를 기준으로 객체의 구조를 정의하듯이 타입을 정의
-> 구조적 타입 시스템
특정 프로퍼티를 상황에 따라 생략하고 싶을 때 프로퍼티 이름뒤에 ?를 붙여준다.
-> 선택적 프로퍼티
let user: {
id?: number; // 선택적 프로퍼티가 된 id
name: string;
} = {
id: 1,
name: "이정환",
};
user = {
name: "홍길동",
};
읽기 전용 프로퍼티
특정 프로퍼티를 읽기 전용으로 만들고 싶을 때 readonly키워드를 붙인다.
let user: {
id?: number;
readonly name: string; // name은 이제 Readonly 프로퍼티가 되었음
} = {
id: 1,
name: "이정환",
};
user.name = "dskfd"; // 오류 발생
타입 단언
- 다른 언어의 형변환과 유사하지만, 특별한 검사를 하거나 데이터를 재구성하지는 않는다.
- TypeScript의 타입 단언은 단지 컴파일러에게 “이 값은 내가 말하는 타입이니 타입 검사를 통과시켜줘”라고 지시하는 것뿐이며, 실제 실행 시에는 아무런 영향을 주지 않는다.
- 2가지 형태가 있다.
- “angle-bracket” 문법
- as-문법
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let someValue2: any = "this is a string";
let strLength2: number = (someValue2 as string).length;
초기화 할 때 빈 객체를 넣고 싶은 경우 타입스크립트에서는 허용하지 않는다. -> 특정 객체 타입이라고 단언해줘야 한다.
type Persion = {
name: string;
age: number;
}
let person: Persion = {}; // 에러
let person = {} as Person;
person.name = "";
persion.age = 23;
초과 프로퍼티일 경우에도 요긴하게 사용이 가능하다.
type Dog = {
name: string;
color: string;
}
let dog: Dog = {
name: "돌돌이",
color: "brown",
breed: "진도",
} as Dog
타입 선언에도 규칙이 있는데 값 as 타입형식의 단언식을 A as B로 표현했을 때 두가지 조건 중 한가지를 반드시 만족해야 한다.
- A가 B의 슈퍼타입이다.
- A가 B의 서브타입이다.
let num1 = 10 as never; // ✅
let num2 = 10 as unknown; // ✅
let num3 = 10 as string; // ❌
Non Null 단언
지금까지 살펴본 값 as 타입 형태를 따르지 않는 단언이다.
값 뒤에 느낌표를 붙여주면 이 값이 undefined이거나 null이 아닐 것으로 단언할 수 있다.
3.2 인터페이스
- 인터페이스란 타입 별칭과 동일하게 타입에 이름을 지어주는 또 다른 문법
- 타입 별칭과 문법만 조금 다를 뿐 기본적인 기능은 거의 같다.
- Typescript의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞추고 있다.
- 덕 타이핑 or 구조적 서브 타이핑이라 부른다.
- 타입들의 이름을 짓는 역할을 하고 코드 안의 계약을 정의하는 것 뿐만 아니라 프로젝트 외부에서 사용하는 코드의 계약을 정의하는 강력한 방법
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
// interface사용
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
// 선택적 프로퍼티
interface SquareConfig {
color?: string;
width?: number;
}
// 읽기 전용 프로퍼티
interface Point {
readonly x: number;
readonly y: number;
}
컴파일러는 최소한 필요한 프로퍼티가 있는지와 타입이 잘 맞는지만 검사
readonly vs const
- readonly => 프로퍼티, const => 변수
주의할 점
- 인터페이스는 대부분의 상황에 타입
초과 프로퍼티 검사
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
- 객체리터럴은 다른 변수에 할당할 때나 인수로 전달할 때 특별한 처리를 받고 초과 프로퍼티 검사를 받는다.
- 개상 타입이 갖고있지 않은 프로퍼티를 갖고 있으면, 에러가 발생
- 객체 리터럴은 중괄호를 사용하여 객체를 직접 정의하는 방법
- 객체 변수와 헷갈릴 수 있는데, 객체 변수는 초과 프로퍼티 검사를 하지 않는다. (우회방법)
- 우회 방법
- 객체를 새로운 변수에 할당
- 타입단언 사용하기
- 인덱스 시그니처 활용
함수 타입
- 프로퍼티로 객체를 기술하는 것 외에, 인터페이스는 함수 타입을 설명할 수 있다.
- 함수 매개변수들은 같은 위치에 대응되는 매개변수끼리 한번에 하나씩 검사한다.
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
// 매개변수 이름이 같을 필요는 없다.
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
인덱서블 타입
- 객체, 배열과 같은 경우 속성이 많이 들어가면 하나하나 타이핑 해줄 수 없는 경우가 존재한다.
- 또는 어떤 속성이 들어갈 지 확정지을 수 없는 경우도 존재한다.
- [인덱서]의 타입은 string과 number만 지정할 수 있다.
- 객체의 key는 문자만 되고(Object.key), 배열은 인덱스(array[0])는 숫자이기 때문이다.
interface PersonIndex {
[key: string]: string; // key가 string, value가 string
}
const people: PersonIndex = {
name: "momo",
job: "entrepreneur",
//age: 17, // error! 'number' 형식은 'string' 형식에 할당할 수 없습니다.
};
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // 성공, length는 숫자입니다
name: string; // 성공, name은 문자열입니다
}
클래스 타입
인터페이스 구현
인터페이스를 사용하는 일반적인 방법은 typescript에서도 가능
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
인터페이스 확장
- 확장 시 다음과 같이 사용한다
- 재정의 또한 가능하다.
- 여러개의 인터페이스를 확장하는 것 또한 가능하다
- 여러번 중복된 이름으로 선언한 경우 인터페이스가 하나로 합쳐진다.
- 동일한 이름의 인터페이스들이 동일한 이름의 프로퍼티를 서로 다른 타입으로 정의한다면 오류가 발생
interface Animal {
name: string;
color: string;
}
interface Dog extends Animal {
breed: string;
}
interface Cat extends Animal {
isScratch: boolean;
}
interface Chicken extends Animal {
isFly: boolean;
}
interface Animal {
name: string;
color: string;
}
interface Dog extends Animal {
name: "doldol"; // 타입 재 정의
breed: string;
}
interface DogCat extends Dog, Cat {}
const dogCat: DogCat = {
name: "",
color: "",
breed: "",
isScratch: true,
};
interface Person {
name: string;
}
interface Person { // ✅
age: number;
}
// 결과
interface Person {
name: string;
age: number;
}
// 아래는 충돌
interface Person {
name: string;
}
interface Person {
name: number;
age: number;
}
하이브리드 타입
클래스를 확장한 인터페이스
3.3 함수
함수 타입
- 자바스크립트에서 함수를 소개하는 방식과 비슷한대 대신 타입만 추가한다.
- 어떤 타입의 매개변수를 받고, 어떤 타입의 값을 반환하는지 이야기하면 된다.
- 함수의 반환값 타입은 자동으로 추론되기 때문에 생략해도 괜찮다.
- 함수의 초기값이 있는 경우 타입이 자동으로 추론된다.
- 매개변수의 이름 뒤에 물음표(?)를 붙여주면 선택적 매개변수가 되어 생략이 가능하다.
- 아래 예제의 경우 tall타입은 number | undefined이다.
- number타입의 값일 거라고 기대하고 사용하려면 다음과 같이 타입 좁히기가 필요하다.
- 선택적 매개변수를 사용할 경우 필수 변수앞에 올 수 없고 반드시 뒤에 배치해야 한다.
- 아래 예제의 경우 tall타입은 number | undefined이다.
- rest 파라미터의 타입은 아래와 같이 정의한다.
- 만약 나머지 매개변수의 길이를 고정하고 싶다면 튜플 타입을 이용해도 된다.
// function func(a: number, b: number): number {
function func(a: number, b: number) {
return a + b;
}
const add2 = (a: number, b: number): number => a + b;
const add3 = (a: number, b: number) => a + b;
function introduce(name = "이정환", tall?: number) {
console.log(`name : ${name}`);
if (typeof tall === "number") {
console.log(`tall : ${tall + 10}`);
}
}
introduce("이정환");
function getSum(...rest: number[]) {
let sum = 0;
rest.forEach((it) => (sum += it));
return sum;
}
function getSum2(...rest: [number, number, number]) {
let sum = 0;
rest.forEach((it) => (sum += it));
return sum;
}
함수 타입 표현식과 호출 시그니처
- 함수 타입을 타입 별칭과 함께 별도로 정의할 수 있다. -> 함수 타입 표현식
- 이렇게 함수 타입 표현식을 이용하면 함수 선언 및 구현 코드와 타입 선언을 분리할 수 있어 유용
type Add = (a: number, b: number) => number;
const add: Add = (a, b) => a + b;
- 함수 타입 표현식은 다음과 같이 여러개의 함수가 동일한 타입을 갖는 경우에 요긴하게 사용된다.
const add = (a: number, b: number) => a + b;
const sub = (a: number, b: number) => a - b;
const multiply = (a: number, b: number) => a * b;
const divide = (a: number, b: number) => a / b;
type Operation = (a: number, b: number) => number;
const add: Operation = (a, b) => a + b;
const sub: Operation = (a, b) => a - b;
const multiply: Operation = (a, b) => a * b;
const divide: Operation = (a, b) => a / b;
호출 시그니처
- 호출 시그니처는 함수 타입 표현식과 동일하게 함수의 타입을 별도로 정의하는 방식
- 자바스크립트에서는 함수도 객체이기 때문에, 위 코드처럼 객체를 정의하듯 함수의 타입을 별도로 정의 가능
- 호출 시그니처 아래에 프로퍼티를 추가 정의하는 것도 가능
- 일반객체를 의미하는 타입으로 정의되며 이를 하이브리드 타입이라고 부른다.
type Operation2 = {
(a: number, b: number): number;
name: string;
};
const add2: Operation2 = (a, b) => a + b;
const sub2: Operation2 = (a, b) => a - b;
const multiply2: Operation2 = (a, b) => a * b;
const divide2: Operation2 = (a, b) => a / b;
add2(1, 2);
add2.name;
함수 타입의 호환성
- 함수 타입의 호환성이란 특정 함수 타입을 다른 함수 타입으로 괜찮은지 판단하는 것을 의미
- 다음 2가지 기준으로 함수 타입의 호환성을 판단
- 두 함수의 반환값 타입이 호환되는가
- 두 함수의 매개변수의 타입이 호환되는가
기준 1 : 두 함수의 반환값 타입이 호환 되는가?
A의 반환값 타입이 B 반환값 타입의 슈퍼타입이라면 두 타입은 호환된다.
type A = () => number;
type B = () => 10;
let a: A = () => 10;
let b: B = () => 10;
a = b; // ✅
b = a; // ❌
기준 2 : 매개변수의 타입이 호환되는가?
매개변수의 타입이 호환되는지 판단할 때에는 두 함수의 매개변수의 개수가 같은지 다른지에 따라 두가지 유형으로 나뉜다.
매개변수의 개수가 같을 때
두 함수 타입 C와 D가 있다고 가정할 때 두 타입의 매개변수의 개수가 같다면 C 매개변수의 타입이 D 매개변수 타입의 서브 타입일 때에 호환된다.
type C = (value: number) => void;
type D = (value: 10) => void;
let c: C = (value) => {};
let d: D = (value) => {};
c = d; // ❌
d = c; // ✅
매개변수의 개수가 다를 때
type Func1 = (a: number, b: number) => void;
type Func2 = (a: number) => void;
let func1: Func1 = (a, b) => {};
let func2: Func2 = (a) => {};
func1 = func2; // ✅
func2 = func1; // ❌
함수 오버로딩
- 함수 오버로딩이란 하나의 함수를 매개변수의 개수나 타입에 따라 다르게 동작하도록 만드는 문법
- 함수 오버로딩을 구현하려면 버전별 오버로드 시그니쳐를 만들어 줘야 한다.
- 오버로드 시그니쳐를 만들었다면 다음으로는 구현 시그니쳐를 만들어줘야 한다.
- 구현 시그니쳐는 실제로 함수가 어떻게 실행될 것인지를 정의하는 부분이다.
- 구현 시그니쳐의 매개변수 타입은 모든 오버로드 시그니쳐와 호환되도록 만들어야 한다.
- 아래의 예시에서는 첫번째 오버로드 시그니쳐와 호환되도록 만들어 주었다.
// 버전들 -> 오버로드 시그니쳐
function func(a: number): void;
function func(a: number, b: number, c: number): void;
// 실제 구현부 -> 구현 시그니쳐
function func(a: number, b?: number, c?: number) {
if (typeof b === "number" && typeof c === "number") {
console.log(a + b + c);
} else {
console.log(a * 20);
}
}
func(1); // ✅ 버전 1 - 오버로드 시그니쳐
func(1, 2); // ❌
func(1, 2, 3); // ✅ 버전 3 - 오버로드 시그니쳐
사용자 정의 타입 가드
- 사용자 정의 타입가드란 참 또는 거짓을 반환하는 함수를 이용해 우리 입맛대로 타입 가드를 만들 수 있도록 도와주는 타입스크립트의 문법
type Dog = {
name: string;
isBark: boolean;
};
type Cat = {
name: string;
isScratch: boolean;
};
type Animal = Dog | Cat;
function warning(animal: Animal) {
if ("isBark" in animal) {
console.log(animal.isBark ? "짖습니다" : "안짖어요");
} else if ("isScratch" in animal) {
console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
}
}
- 위와 같이 in연산자를 이용해 타입을 좁히는 방식은 좋지않다.
- 예를 들어 만약 Dog타입의 프로퍼티가 중간에 이름이 수정되거나 추가 또는 삭제될 경우에는 타입가드가 제대로 동작하지 않기 때문이다
- 따라서 이럴 때는 다음과 같이 함수를 이용해 커스텀 타입 가드를 만들어 타입을 좁히는 방식이 좋다.
...
// Dog 타입인지 확인하는 타입 가드
function isDog(animal: Animal): animal is Dog {
return (animal as Dog).isBark !== undefined;
}
// Cat 타입인지 확인하는 타입가드
function isCat(animal: Animal): animal is Cat {
return (animal as Cat).isScratch !== undefined;
}
function warning(animal: Animal) {
if (isDog(animal)) {
console.log(animal.isBark ? "짖습니다" : "안짖어요");
} else {
console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
}
}
animal is Dog를 정의하면 이 함수가 true 반환 시 조건문 내부에서는 이 값이 Dog타입임을 보장한다는 의미가 된다.
3.4 리터럴 타입
- string,number 처럼 범용적으로 많은 값을 포함하는 타입뿐만아니라 딱 하나의 값만 포함하는 타입
// 값을 10만 사용할 수 있음
let numA: 10 = 10;
3.5 유니언과 교차타입
3.6 클래스
3.7 열거형
3.8 제네릭
4. 핸드북 레퍼런스
4.1 고급 타입
4.1.0 대수 타입
여러개의 타입을 합성해서 만드는 타입
합집합 타입과 교집합 타입이 존재하는데 합집합은 Union타입, 교집합은 Intersection타입이라 부른다.
4.1.1 교차 타입
let variable: number & string;
type Dog = {
name: string;
color: string;
};
type Person = {
name: string;
language: string;
};
type Intersection = Dog & Person;
let intersection1: Intersection = {
name: "",
color: "",
language: "",
};
기본 타입들 간에는 서로 공유하는 교집합이 없기 때문에 이런 인터섹션 타입은 보통 객체 타입들에 자주 사용된다.
4.1.2 유니언 타입
// 합집합 타입 - Union 타입
let a: string | number | boolean;
a = 1;
a = "hello";
a = true;
let arr: (number | string | boolean)[] = [1, "hello", true];
type Dog = {
name: string;
color: string;
};
type Person = {
name: string;
language: string;
};
type Union1 = Dog | Person;
4.1.3 타입 가드와 차별 타입
타입 좁히기
다음과 같은 함수가 있을 때 value의 타입이 number | string이므로 함수 내부에서 다음과 같이 value가 number타입이거나 string타입일 것이라고 기대하고 메서드를 사용하려하면 오류가 발생한다.
function func(value: number | string) {
value.toFixed() // 오류
value.toUpperCase() // 오류
}
다음과 같은 조건문을 이용해 value의 타입이 number타입임을 보장해줘야 한다.
type Person = {
name: string;
age: number;
};
function func(value: number | string) {
if (typeof value === "number") {
console.log(value.toFixed());
} else if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (value instanceof Date) {
console.log(value.getTime());
} else if(value && "age" in value) {
console.log(`${value.name}은 ${value.age}살 입니다.`)
}
}
- if(typeof === …)처럼 조건문과 함께 사용해 타입을 좁히는 이런 표현들을 타입 가드라고 부른다.
- instanceof를 이용하면 내장 클래스 타입을 보장하는 타입가드를 생성 가능하다.
- 직접 만든 타입과 함께 사용하려면 다음과 같이 in 연산자를 이용해야 한다.
4.1.4 널러블 타입
4.1.5 타입 별칭
- 타입 정의를 마치 변수처럼 하게 해준다.
- 동일한 스코프에 동일한 이름의 타입 별칭 선언 불가
- 스코프가 다르면 중복된 이름으로 여러개 별칭 설정해도 상관 없다.
type User = {
id: number;
name: string;
nickname: string;
birth: string;
bio: string;
location: string;
};
let user: User = {
id: 1,
name: "이정환",
nickname: "winterlood",
birth: "1997.01.07",
bio: "안녕하세요",
location: "부천시",
};
function test() {
type User = string;
}
4.1.6 문자열 리터럴 타입
4.1.7 숫자 리터럴 타입
4.1.8 열거형 멤버 타입
4.1.9 판별 유니온
- 서로소 유니온 타입이라고도 한다.
- 교집합이 없는 타입들 즉, 서로소 관계에 있는 타입들을 모아 만든 유니온 타입을 말한다.
- 아래는 String Literal 타입을 추가한 type들을 활용해 타입체크를 했다.
type Admin = {
tag: "ADMIN";
name: string;
kickCount: number;
};
type Member = {
tag: "MEMBER";
name: string;
point: number;
};
type Guest = {
tag: "GUEST";
name: string;
visitCount: number;
};
type User = Admin | Member | Guest;
function login(user: User) {
if ("kickCount" in user) {
// Admin
console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
} else if ("point" in user) {
// Member
console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
} else {
// Guest
console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
}
}
function login2(user: User) {
switch (user.tag) {
case "ADMIN": {
console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
break;
}
case "MEMBER": {
console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
break;
}
case "GUEST": {
console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
break;
}
}
}
4.1.10 다형성 this타입
4.1.11 인덱스 타입
인덱스 시그니처
객체 타입을 유연하게 정의할 수 있도록 돕는 특수한 문법
type CountryCodes = {
Korea: string;
UnitedState: string;
UnitedKingdom: string;
// (... 약 100개의 국가)
Brazil : string
};
type CountryCodes = {
[key: string]: string;
Korea: number;
};
let countryCodes: CountryCodes = {
Korea: "ko",
UnitedState: "us",
UnitedKingdom: "uk",
// (... 약 100개의 국가)
Brazil : 'bz'
};
4.1.12 매핑 타입
4.1.13 조건부 타입
4.2 선언 병합
4.3 데코레이터
4.4 유틸리티 타입
4.5 이터레이터와 제네레이터
4.6 JSX
4.7 믹스인
4.8 모듈
4.9 모듈 해석
4.10 네임스페이스
4.11 네임스페이스와 모듈
4.12 심볼
4.13 트리플-슬래시 지시자
4.14 타입 호환성
4.15 타입 추론
- 타입 스크립트는 타입이 정의되어 있지 않은 변수의 타입을 자동으로 추론한다.
- 함수의 매개변수 타입은 자동으로 추론할 수 없다
- 타입추론이 불가능한 변수에는 암시적으로 any타입으로 추론
- 엄격한 타입 검사모드는 any타입 추론을 오류로 본다.
- 구조 분해 할당하는 상황에서도 타입 추론이 잘 된다.
- 초기값을 생략하면 암시적으로 any타입으로 추론한다.
- const상수의 추론의 경우는 let으로 선언한 변수와는 다른 방식으로 추론된다.
- 상수는 초기화 때 설정한 값을 변경할 수 없기 때문에 가장 좁은 타입으로 추론된다.
- 다양한 타입의 요소를 담은 배열을 변수의 초기값으로 설정하면, 최적의 공통 타입으로 추론된다.
const num = 10; // Number Literal타입으로 추론
const str = "hello"; // "hello" String Literal 타입으로 추론
let arr = [1, "string"];// (string | number)[] 타입으로 추론
4.16 타입스크립트와 DOM
4.17 변수 선언
5. TS for OOP Programmers, Functional Programmers
Tip!
1. 인터페이스와 타입을 나눈 이유
interface와 type을 나눈 이유는 확장성, 선언적 성격, 그리고 유연성 때문
- interface와 type의 주요 차이점
- interface
- 객체 구조 정의에 최적화
- 확장 가능
extends로 다른 인터페이스를 상속받을 수 있음
- 병합가능
- 같은 이름의 인터페이스를 여러번 선언하면 자동으로 병합 됨
- 클래스와의 강한 견광성
implements를 사용해 클래스가 특정 구조를 따르게 강제할 수 있음
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
position: string;
}
const employee: Employee = { name: "Alice", age: 30, position: "Developer" };
- 타입
- 좀더 범용적이고, 인터페이스보다 더 다양한 타입을 정의하는 데 사용됨
- 유니온(|) 및 인터섹션(&) 지원
- 여러 타입 조합 가능
- 기본 타입에도 적용 가능
- 인터페이스는 객체 구조만 정의하지만,
type은 그 이상을 다룸
- 인터페이스는 객체 구조만 정의하지만,
- 확장 방식이 인터페이스와 다름
- & 연산자로 확장 가능하지만 병합은 안됨
type ID = string | number; // 유니온 타입
type Person = { name: string; age: number };
type Employee = Person & { position: string }; // 인터섹션 타입
const employee: Employee = { name: "Bob", age: 25, position: "Designer" };
- 둘 다 존재하는 이유
-
- 역사적 이유
- 초기 타입 스크립트에는 interface만 있었음
- 이후 유니온 타입, 튜플같은 복잡한 타입 정의를 위해 type이 추가됨
-
- 사용 목적이 다름
- 객체의 구조를 정의할 때는 interface를 주로 사용
- 유니온, 튜플, 기본 타입 조합등은 type을 사용
-
- 병합 여부
- interface는 병합됨
- type은 병합되지 않음
- 정리
- 객체 구조를 정의할 떄 ->
interface(확장성과 클래스 연계성이 필요할 때) - 유니온 타입, 튜플, 기타 복잡한 타입을 다룰 때
type
요즘은 대부분의 경우에 interface를 기본적으로 사용하고, 필요할 때만 type활용
정확히 그거야. 🎯 Next.js + TypeScript 환경에서 외부 전역 변수 Module을 내부에서 일부 기능만 사용할 경우, 아래처럼 부분 선언만 해주면 충분하다:
✅ 맞는 선언 방식 ts 복사 편집 declare const Module: { XDSetMouseState: (state: number) => void; }; 실제 Module 객체에는 더 많은 속성이 있더라도,
TypeScript는 당신이 쓰는 부분만 알면 됨 🧠
타입은 열린 구조이기 때문에, 나머지는 무시하고도 컴파일 가능
📌 왜 이렇게 해도 되냐? TypeScript는 컴파일 타임에만 타입 검사함
런타임에서는 실제 JS 객체만 보면 되므로 정의된 타입보다 속성이 더 많아도 문제 없음
🔐 팁: 구조 더 유연하게 하고 싶다면? 방법 A: 확장 가능하게 열어두기 ts 복사 편집 declare const Module: { XDSetMouseState: (state: number) => void; [key: string]: any; // 다른 함수들도 있을 수 있음 }; 방법 B: 선택적 정의 ts 복사 편집 interface ModuleType { XDSetMouseState?: (state: number) => void; // …추가 가능 } declare const Module: ModuleType; ✅ 정리 상황 타입 선언 방식 일부 기능만 사용 필요한 함수만 declare 타입 경고 없이 유연하게 [key: string]: any 추가 런타임 검사는? 없음 (오로지 컴파일용) 필요하면 .d.ts 파일에 깔끔하게 빼주는 것도 가능. 프로젝트 규모 커지면 유지보수 훨씬 쉬워짐.
Module 전체 구조를 추론하거나 자동 생성하고 싶으면 해당 JS 파일도 받아줄 수 있어 🔍 진행할까?
TODO: 정리