타입스크립트 핸드북을 요약 정리한 내용입니다.
let str : string = 'hi'
let num : number = 10;
let isLoggedIn : boolean = false;
위와 같이
:
를 이용하여 자바스크립트 코드에 타입을 정의하는 방식을 타입 표기(Type Annotation)라고 한다.
let arr : number[] = [1,2,3];
let arr = Array<number> = [1,2,3];
튜플은 길이와 요소가 고정되어있는 배열이다.
let arr : [string, number] = ['hi', 10]
특정 상수 값들의 집합
enum fruits {apple, banana, orange}
let food : fruits = fruites.apple;
enum fruits {apple, banana, orange}
let food : fruits = fruites[0];
// apple의 인덱스번호를 2로 지정
// banana,orange는 3,4가 된다.
enum fruits {apple=2, banana, orange}
let food : fruits = fruits[2]; // apple
let food : fruits = fruits[4]; // orange
타입이 지정되어있지 않은 자바스크립트 코드를 타입스크립트에 적용할 때 사용하기 좋은 타입이다.
즉, 모든 타입을 허용한다.
let str: any = 'hi';
let num: any = 10;
let arr: any = ['a', 2, true];
반환 값이 없는 함수를 설정할 때 사용하는 타입이다.
변수를 선언할때는 undefined
와 null
만 할당가능하다.
let unuseful: void = undefined;
function notuse(): void {
console.log('sth');
}
함수가 절대 끝나지 않는 의미를 가질때 사용한다.
// 이 함수는 절대 함수의 끝까지 실행되지 않는다는 의미
function neverEnd(): never {
while (true) {
}
}
타입스크립트에서 함수를 선언할 때 3가지의 타입을 정의할 수 있다.
타입스크립트에서는 함수의 인자를 모두 선언해 주어야한다.
그러므로 매개변수를 설정했다면 매개변수의 값이 제대로 넘어왔는지 확인을 해주어야 한다.
만약 함수가 가지는 인자의 갯수가 때에 따라 달라진다면 ?
키워드를 사용해 정의 할 수 있다.
function sum(a: number, b?: number): number {
return a + b;
}
sum(10, 20); // 30
sum(10, 20, 30); // error, too many parameters
sum(10); // 10
sum
은 매개변수로 a
와 b
혹은 a
하나만으로도 가질 수 있다.
다음과 같이 매개변수를 정의와 동시에 초기화할 수 있다.
function sum(a: number, b = '100'): number {
return a + b;
}
sum(10, undefined); // 110
sum(10, 20, 30); // error, too many parameters
sum(10); // 110
function sum(a: number, ...nums: number[]): number {
let totalOfNums = 0;
for (let key in nums) {
totalOfNums += nums[key];
}
return a + totalOfNums;
}
console.log(sum(1,2,3,4,5)); // 15
console.log(sum(1,2,3)); // 6
에러정리
const arr1 : number[] = [1,2,3,4,5]; const arr2 : number[] = [1,2,3]; console.log(sum(arr1)); console.log(sum(arr2));
...nums : number[]
를 매개변수로 받기 때문에number[]
으로 선언된 변수를sum
함수의 매개변수로 보내려고 했지만 컴파일에러가 발생했다.
...nums : number[]
는 nums를 분해했을 때number[]
타입을 가져야하는 변수이다.
즉,...nums: number[]
는[[1,2,3]]
의 형태를 가져야만nums
를 펼쳤을때number[]
타입이 나오게 되는 것이다.--> nums : [number[]]
--> ...nums : number[]그러므로 매개변수에 선언된
a:number
에 의해 매개변수number
타입인 1,2,3 등이 올수 있으며 이들은 ...nums :number[]
에 의해[1,2,3]
배열의 형태로 받게 된다.
타입스크립트에서 this
를 사용하기 위해선 다음과 같은 문법을 사용한다 .
function 함수명(this: 타입) {
// ...
}
this를 사용해 인터페이스에 선언된 변수의 getter역할을 할 수 있습니다.
interface Iam {
name: string;
age: number;
getAge(this: Iam): () => {};
}
let iam: Iam = {
name: 'seungha',
age: 26,
getAge: function(this: Iam) {
return () => {
return this.age;
}
}
}
let myAge = iam.getAge();
let age = myAge();
console.log(age); // 26
콜백으로 함수가 전달되었을때도 this
를 사용해 구분해줘야한다.
interface UIElement {
// 아래 함수의 `this: void` 코드는 함수에 `this` 타입을 선언할 필요가 없다는 의미입니다.
addClickListener(onclick: (this: void, e: Event) => void): void;
}
class Handler {
info: string;
onClick(this: Handler, e: Event) {
// 위의 `UIElement` 인터페이스의 스펙에 `this`가 필요없다고 했지만 사용했기 때문에 에러가 발생합니다.
this.info = e.message;
}
}
let handler = new Handler();
uiElement.addClickListener(handler.onClick); // error!
function logAge(obj: { age: number }) {
console.log(obj.age); // 28
}
logAge()
함수는 number
타입을 속성으로 가지는 객체를 인자로 받는다.
let person = { name: 'Capt', age: 28 };
logAge(person); // 28
인터페이스를 적용하면 logAge()
의 인자를 좀 더 명시적으로 작성할 수 있다.
interface personAge {
age: number;
}
function logAge(obj: personAge) {
console.log(obj.age);
}
let person = { name: 'Capt', age: 28 };
logAge(person);
logAge()
함수는 personAge
라는 타입을 인자로 가져야하고 personAge
인터페이스는 number
타입의 age
속성을 가지고 있다.
인터페이스를 인자로 받아 사용하면 인자로 받은 객체의 속성갯수(person 속성 두 개) 와 인터페이스의 속성(personAge 속성 한 개) 의 개수가 일치하지 않아도 된다.
즉, 객체가 가진 속성 중 인터페이스에 정의된 속성, 타입을 만족하는 것이 있기만 하면 되므로 속성의 갯수와 순서를 신경쓰지 않아도 된다.
인터페이스를 사용 할 때 인터페이스에 정의된 속성을 모두 사용하지 않아도 된다.
이를 옵션 속성이라고 한다.
옵션 속성은 ?
를 붙여사용한다.
interface CraftBeer {
name: string;
hop?: number;
}
let myBeer = {
name: 'Saporo'
};
인터페이스의 속성이 두개 임에도 불구하고?
를 붙인 hop
속성을 정의하지 않아도 선언이 가능하다.
이처럼 옵션속성은 인터페이스를 사용할 때 속성을 선택적을 적용할 수 있기도 하지만, 인터페이스에 정의되지 않은 속성에 대해선 오류를 표시해주어 속성을 인지 시켜주기도한다.
인터페이스로 객체를 처음 생성할 때만 값을 할당하고 그 이후에는 변경하지 않는 속성에는 readOnly
를 붙여준다.
interface CraftBeer {
readonly brand: string;
}
readOnly
를 수정하려고 하면 에러가 발생한다.
let myBeer: CraftBeer = {
brand: 'Belgian Monk'
};
myBeer.brand = 'Korean Carpenter'; // error!
배열을 선언할 때 ReadonlyArray<T>
타입을 사용하면 읽기 전용 배열을 생성할 수 있다.
let arr: ReadonlyArray<number> = [1,2,3];
arr.splice(0,1); // error
arr.push(4); // error
arr[0] = 100; // error
클래스가 일정 조건을 만족하도록 타입 규칙을 지정할 수 있다.
interface CraftBeer {
beerName: string;
nameBeer(beer: string): void;
}
class myBeer implements CraftBeer {
beerName: string = 'Baby Guinness';
nameBeer(b: string) {
this.beerName = b;
}
constructor() {}
}
interface Person {
name : string
}
interface Developer extends Person {
skill : string
}
let dev = {} as Developer;
dev.name = 'seungha';
dev.skill = 'ts';
자바스크립트의 유연한 특성을 따라 인터페이스도 여러 타입을 조합하여 만들수 있다.
다음은 함수 타입이면서 객체 타입을 정의하는 인터페이스이다.
interface CraftBeer {
(beer : string) : string;
brand : string;
brew() : void;
}
function myBeer() : CraftBeer {
let my = (function(beer:string) {console.log (`1 beer name: ${beer}`)} ) as CraftBeer;
my.brand = 'beer kitchen';
my.brew = function() { console.log ("2 brew()", `3 brand ${this.brand}`)}
return my;
}
let brewedBeer = myBeer();
brewedBeer("function type beer");
brewedBeer.brand = "beer brand";
brewedBeer.brew();
이넘은 특정 값들의 집합을 의미하는 자료형이다.
enum Message {
// auto-incrementing
Yes, // or Yes = 0
No // or No = 1
}
function respond(name : string , message : Message){
console.log(name, message);
}
respond("seungha", Message.Yes)
//"seungha", 0
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
문자형 이넘은 숫자형 이넘과 달리 이넘 값 모두를 특정 문자 혹은 다른 이넘 값으로 초기화를 해줘야한다.
또한, 숫자형 이넘과 같은 auto-incrementing이 없으므로 주의해야한다.
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
문자와 숫자를 혼합하여 이넘을 생성할 수 있으나 권고하는 방식은 아니다.
최대한 같은 타입으로 이루어진 이넘을 생성하자.
이넘은 런타임시에는 객체이다
그러므로 이넘 타입을 typeof
로 사용하면 이넘 타입이 아닌 객체 자체가 타입이 되어버린다.
즉, 인터페이스와 비슷하게 사용할 수 밖에 없어진다.
그럼 이넘 타입을 사용하기 위해선 어떻게 해야할까?
keyof
를 사용해 이넘 객체의 프로퍼티들을 뽑아내자.
enum LogLevel {
ERROR, WARN, INFO, DEBUG
}
// 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;
// LogLevelStrings 이넘 타입 사용
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log('Log level key is: ', key);
console.log('Log level value is: ', num);
console.log('Log level message is: ', message);
}
}
printImportant('ERROR', 'This is a message');
keyof typeof
를 사용하여 이넘 객체 자체를 구하고, 객체의 키를 추출하여 사용하자.
function getAge(age: string | number) {
// ...
}
위 함수는 인자로 문자열 타입이나 숫자 타입이 모두 가능한 함수이다.
|
를 사용하여 타입을 여러개 연결하는 것을 유니온 타입 정의 방식이라고 한다 .
any
를 사용하여 모든 타입을 허용할 수 있지만, 이런경우 타입스크립트가 타입을 추론할 수 없으므로 유니온 타입을 잘 활용하자.
여러타입을 모두 만족하는 하나의 타입을 인터섹션 타입이라고 한다.
interface Person {
name: string;
age: number;
}
interface Developer {
name: string;
skill: number;
}
type Capt = Person & Developer;
&
를 사용하여 두 인터페이스의 정의를 합쳤다.
결과적으로 Capt
의 타입은 다음과 같이 정의된다.
{
name: string;
age: number;
skill: string;
}
interface Person {
name: string;
age: number;
}
interface Developer {
name: string;
skill: string;
}
function introduce(someone: Person | Developer) {
someone.name; // O 정상 동작
someone.age; // X 타입 오류
someone.skill; // X 타입 오류
}
파라미터의 타입이 Person
도 되고 Developer
도 될테니까 age
나 skill
를 사용할 수 있겠지라고 생각할 수 있으나,
공통적으로 들어있는 속성인 name
에만 접근 할 수 있다.
readOnly
를 클래스의 속성에 적용하면 접근만 가능하게 된다.
class Developer {
readonly name: string;
constructor(readonly name: string) {
}
}
따라서 readonly
를 사용하면 생성자 함수에 초기 값 설정을 지정하면서 인자에 readonly
를 함께 추가하여 사용하면 유용하다.
객체의 특정 속성의 접근과 할당을 제어하기 위해선 해당 객체를 클래스로 생성해야한다.
class Developer {
private name: string;
get name(): string {
return this.name;
}
// name속성에 제약사항 추가
set name(newValue: string) {
if (newValue && newValue.length > 5) {
throw new Error('이름이 너무 깁니다');
}
this.name = newValue;
}
}
const josh = new Developer();
josh.name = 'Josh Bolton'; // Error
josh.name = 'Josh';
get
과 set
을 사용하여 할당되는 값을 제어 할 수 있다.
get
만 선언하고set
을 선언하지 않는 경우 자동으로readonly
로 인식된다.
추상 클래스는 인터페이스와 비슷하지만 특정한 클래스의 상속 대상이 되는 역할로, 상위레벨에서 속성등을 정의할 때 사용한다.
abstract class Developer {
abstract coding(): void; // 'abstract'가 붙으면 상속 받은 클래스에서 무조건 구현해야 함
drink(): void {
console.log('drink sth');
}
}
class FrontEndDeveloper extends Developer {
coding(): void {
// Developer 클래스를 상속 받은 클래스에서 무조건 정의해야 하는 메서드
console.log('develop web');
}
design(): void {
console.log('design web');
}
}
const dev = new Developer(); // error: cannot create an instance of an abstract class
const josh = new FrontEndDeveloper();
josh.coding(); // develop web
josh.drink(); // drink sth
josh.design(); // design web
제네릭은 타입을 함수의 인자처럼 사용하는 것을 말한다.
function getText(text) {
return text;
}
getText('hi'); // 'hi'
getText(10); // 10
getText(true); // true
위 함수는 text
를 인자로 받아 그대로 반환해준다.
text
는 어떤한 타입이 들어가도 반환이 된다.
이를 제네릭 문법을 적용하면 다음과 같은 형태가 된다.
이처럼 제네릭을 사용하면 any
타입을 사용했을때 타입검사를 하지 않아 어떤 값이 반환되는지 알 수 없게되는 문제를 피할 수 있으며, 가독성도 좋아진다.
function getText<T>(text: T): T {
return text;
}
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);
예를들어 getText<string>('hi');
는 다음과 같이 작동하게 된다.
function getText<string>(text: string): string {
return text;
}
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
위 코드는 text
의 length를 확인하기 알기위해서 작성한 코드이다.
하지만 컴파일 에러가 발생한다.
위 코드를 다시 보자.
<T>
는 any
처럼 작동한다.
즉, 문자열이나 배열 숫자가 들어올 수는 있지만 숫자인 number
가 들어왔을 때에는 .length
가 유효하지 않기때문이다.
이런 경우에는 제네릭에 타입을 주면 된다.
function logText<T> (text : Array<T>) : Array<T> {
console.log(text.length);
return text;
}
제네릭타입으로 인자를 받고 인자 값을 배열형태로 받는다.
가령, [1,2,3]이라는 숫자 배열을 받게된다면 반환값은 number
가 될 것이다.
function logText<T> (text : T) : T {
return text;
}
// 1
let str : <T>(text : T) => T = logText;
// 2
let str : {<T>(text: T) : T} = logText;
logText
함수를 사용하는 두 코드는 같은 의미를 가진다.
logText
함수는 <T>
제네릭이며 인자로 가지는 text
는 T
제네릭이고 리턴타입 또한 T
이다.
이 방식으로 인터페이스를 작성할 수 있다.
interface GenericLogTextFn {
<T>(text: T): T;
}
function logText<T> (text : T) : T {
return text;
}
let my : GenericLogTextFn = logText;
GenericLogTextFn
인터페이스가 없었다면
let my : {<T>(text : T) : T} = logText;
처럼 타입을 명시하면서 적어야 했을 것이다.
GenericLogTextFn
를 적용하여 가독성을 높일 수 있다.
제네릭은 인터페이스 뿐만 아니라 클래스도 생성할 수 있다.
이넘과 네임스페이스는 제네릭으로 생성할 수 없다.
class GenericMath<T> {
pi: T;
sum: (x: T, y: T) => T;
}
let math = new GenericMath<number>();
클래스 명에 <T>
를 함께 적어주고, 클래스로 인스턴스를 생성할 때 타입을 함께 지정해주면 된다.
제네릭으로 선언한 인자에 어떤 타입이 들어왔는지 확인 할 수 있는 방법을 알아보자.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
앞서 말했듯이 T
에는 어떤 타입이 들어올지 모르므로 .length
에서 오류가 난다.
동작하는 인자만 받을 수 있도록 아래와 같이 작성할 수 있다.
interface LengthWise {
length : number ;
}
function logText<T extends LengthWise>(text: T) :T {
console.log(text.length);
return text;
}
logText({ length: 0, value: 'hi' });
// `text.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
length
에 대해 동작하는 인자만 받을 수 있도록 인터페이스를 추가 해주 었다.
두개의 객체를 비교할 때도 제네릭을 사용할 수 있다.
function getProperty<T, O extends keyof T> (obj : T, key : O) {
return obj[key];
}
let o = {a:1,b:2,c:3};
getProperty(o, "a");
getProperty(o, "x"); //error;
위 코드에서는keyof
를 사용하여 두번째 인자에는 첫번째 인자의 키에 해당하는 객체만이 올 수 있도록 제한하였다.
타입스크립트는 Best Common Type 알고리즘에 의해 타입을 추론하게 된다.
let arr = [0, 1, null]
arr
은 number
과 null
로 이루어진 배열이다.
이 때 Best Common Type
알고리즘으로 다른 타입들과 가장 잘 호환되는 타입을 선정하여
arr
의 타입을 let arr: (number | null)[]
로 추론한다.
타입스크립트는 문맥(코드 위치)으로도 타입을 결정한다.
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //<- OK
console.log(mouseEvent.kangaroo); //<- Error!
};
타입스크립트는 window.onmousedown
에 할당되는 함수의 타입을 추론하기 위해 window.onmousedown
타입을 먼저 검사한다.
window.onmousedown
타입은 마우스의 이벤트와 관련이 있다고 알아내면 마우스에 button
은 있지만 kangaroo
속성은 없다고 판단한다.
타입스크립트가 타입체킹을 할 때는 값의 형태에 기반하여 이루어진다.
이것을 Duck Typing 또는 Structural Subtyping 이라고 한다.
- Duck Typing
객체의 변수 및 메서드의 집합이 객체의 타입을 결정하는 것을 의미. 동적 타이핑의 한 종류- Structural Subtyping
객체의 실제 구조나 정의에 따라 타입을 결정하는 것을 의미
타입호환이란 타입스크립트에서 특정 타입이 다른 타입에 잘 맞는지를 의미한다.
interface Ironman {
name: string;
}
class Avengers {
name: string;
}
let i: Ironman;
i = new Avengers();
자바스크립트는 객체 리터럴이나 익명함수를 사용하여 작동하기 때문에, 명시적으로 타입을 지정하는 것보다는 코드의 구조관점으로 타입을 지정한다.
이것이 위 코드에서 Ironman
과 Avengers
는 상속관계이 있지 않지만 에러가나지 않는 이유이다.
구조적 타이핑이란 코드 구조 관점에서 타입이 서로 호환되는지의 여부를 판단하는 것이다.
interface Avengers {
name : string;
}
let hero : Avengers ;
let capt = {name : "seunghaahha", location : "seoul"}
hero = capt;
Avengers
의 속성에 name
이 있으므로 hero
에 capt
이 대입이 가능하다.
타입스크립트는 컴파일 시점에 타입을 추론할 수 없어도 일단 안전하다고 보는 특성이 있다.