타입스크립트는 자바스크립트에 타입을 부여한 언어로, 코드 작성 단계에서 타입을 체크해 타입 에러를 방지할 수 있다.
타입스크립트는 자바스크립트와 달리 브라우저에서 실행하려면 파일을 한번 변환해야 하며, 컴파일러를 통해 자바스크립트와 100% 호환이 가능하다.
또한, 자바스크립트에서 제공하지 않는 클래스, 인터페이스, 상속 등과 같은 객체 지향 프로그래밍 패턴을 제공함으로써 객체 지향 프로그래밍을 지원한다.
function sum(a, b) {
return a + b;
}
sum(1, 2); // 3
sum("1", "2"); // 12
자바스크립트는 변수에 따로 타입을 지정해주지 않기 때문에 sum 함수의 인자로 숫자가 들어와도, 문자가 들어와도 모두 정상적으로 연산이 되어 결과를 반환한다. 이러한 자바스크립트의 유연함이 편해보일 순 있지만, 코드가 길어지면 오히려 더 많은 오류를 발생시킬 수 있다.
function sum(a: number, b: number) {
return a + b;
}
sum(1,2) // 3
sum('1', '2'); // Error
// 매개변수의 타입을 number로 지정해주었기 때문에 문자열을 넘겨주면 에러 발생
// -> 의도하지 않은 코드의 동작을 예방할 수 있음
하지만, 타입스크립트에서는 지정해둔 타입과 일치하지 않는 데이터를 넘기려고 하면 오류가 발생한다. 때문에 sum함수에 인자로 숫자를 넘겨주었을 땐 정상 작동하지만, 문자열을 넘겨주면 에러가 발생한다. 이처럼 데이터 타입을 엄격하게 지정해줌으로써, 의도하지 않은 코드의 동작을 예방할 수 있는 것이다. 즉, 타입스크립트는 자바스크립트의 한계를 해결한다.
타입스크립트는 자바스크립트의 타입을 그대로 사용하며, 이 외에도 타입스크립트의 새로운 타입들이 추가로 제공된다.
String
: 문자열
Number
: 숫자
Boolean
: 참/거짓
Null
: 의도적으로 비워둔 값
Undefined
: 정의되지 않은 값
Array
: 배열
Object
: 객체
symbol
: 고유하고 수정 불가능한 값 (ES6에서 추가)
Tuple
: 길이와 각 요소의 타입이 정해진 배열
Enum
: 특정 값들의 집합
Any
: 모든 타입, 어느 타입이던지
unknown
: 모르는 타입, 어떤 타입으로 올지 모를 때
Void
: 결과 값을 반환하지 않는 함수 타입
Never
: 항상 오류를 발생시키거나 반환이 없는 함수 타임
타입스크립트에서는 어떤 변수 또는 값의 타입을 지정하기 위해 뒤에 콜론(:)
을 붙여 value: type
의 형태로 타입을 표기한다.
• 문자열 - value: string
• 숫자 - value: number
• boolean - value: boolean
• null - value: null
• undefined - value: undefined
let str: string = 'hi'; // 문자열
let num: number = 0; // 숫자
let t: boolean = true; // boolean
let n: null = null; // null
let u: undefined = undefined; // undefined
• value: type[]
| value: Array<type>
• 여러 타입의 값을 가지는 배열
value: (type1 | type2...)[]
| value: Array<type1 | type2...>
// 배열 타입 표기1
// 타입[]
let arrNum1: number[] = [1,2,3];
let arrStr1: string[] = ["one", "two", "three"];
// 배열 타입 표기2
// 제네릭 - Array<타입>
let arrNum2: Array<number> = [1,2,3];
let arrStr2: Array<string> = ["one", "two", "three"];
// 배열 타입 표기3
// 여러 타입을 동시에 가지는 배열 - Union(|)
let arrMix1: (string | number)[] = ["one", 1, "two", 2, "three", 3];
let arrMix2: Array<string | number> = ["one", 1, "two", 2, "three", 3];
* object는 기본적으로 typeof 연산자가 object로 반환하는 모든 타입을 나타낸다.
• value: object
• 객체의 각 속성들의 타입을 개별적으로 지정
value: { key1: type1, key2: type2... }
let obj: object = {};
// 객체의 각 속성 타입 지정
// name은 문자열, age는 숫자
let person: { name: string, age: number } = {
name: 'unknown',
age: 23
};
• function value(parameter1: type1, parameter2: type2...): type
함수의 매개변수와 리턴 값의 타입을 지정해준다.
// 매개변수 a와 b의 타입은 숫자
// 리턴 값의 타입은 숫자
function sum(a: number, b: number): number {
return a+b;
}
튜플은 배열과 유사하지만 약간의 차이점이 있다. 정해진 타입의 요소 개수 만큼의 타입을 미리 선언후 배열을 표현한다. 즉, 배열의 길이가 고정되고 각 요소의 타입이 지정되어 있는 배열 형식이다.
• 튜플 - value: [type1, type2...]
let tuple1: [string, number] = ['one', 1];
// 정해진 요소의 순서 및 개수가 다르면 에러가 발생
let tuple2: [string, string] = ["Hi", 100]; // Error
// 정의하지 않은 타입이나, 인덱스로 접근할 경우 에러 발생
let tuple3: [string, number] = ['one', 1];
tuple3[1].concat('!'); // tuple3[1]은 숫자이기 때문에 concat를 사용하면 에러
arr[3] = "two"; // 정의되지 않은 인덱스이므로 에러
열거형은 숫자 혹은 문자열 값 집합에 이름을 부여할 수 있는 타입으로, 값의 종류가 일정한 범위로 정해져 있는 경우 유용하다. 기본적으로 0부터 시작하며, 값은 1씩 증가한다.
• enum - enum value { }
enum obj { a, b, c }
console.log(obj)
// 0: "a"
// 1: "b"
// 2: "c"
// 의도적으로 인덱스 변경
enum obj { a=1, b=2, c=3 };
let a: String = obj[1];
enum fruits { Apple, Grape, Peach }
let eat: fruits = fruits.Apple;
any는 모든 타입에 대해서 허용한다. 타입 선언이 어려운 경우 유용할 수 있으나, 컴파일 중 타입 검사를 하지 않기 때문에 에러 발생에 주의해야 한다.
• value: any
let anything: any = 100;
any = 'Hi';
console.log(anything); // 'Hi'
any = true;
console.log(anything); // true
void는 any의 반대 타입으로, 변수에는 undefined와 null만 할당하고, 함수에는 반환 값을 설정할 수 없는 타입이다.
• value: void
let n: void = null;
function unknown(): void {
console.log("unknown");
}
console.log(unknown()); // undefined
never는 절대 발생할 수 없는 타입을 나타내며, 항상 오류를 발생시키거나 절대 반환하지 않는 반환 타입이다.
함수의 끝에 절대 도달하지 않는다는 의미를 지니고 있다.
• value: never
// 이 함수는 절대 함수의 끝까지 실행되지 않는다
function neverEnd(): never {
while (true) { }
}
유니온 타입이란 자바스크립트의 OR 연산자(||)와 같이 'A' 이거나 'B'이다 라는 의미의 타입으로, 타입을 여러 경우로 지정하고 싶을 때 사용한다.
유니온 타입을 지정할 때는 |
를 사용하여 타입을 열거하면 된다.
let types = string | number;
// types 변수의 타입이 string 이거나 number 이거나
인터섹션 타입이란 자바스크립트의 AND 연산자(&&)와 같이 'A'와 'B'이다 라는 의미의 타입으로, 이미 존재하는 여러 타입을 모두 만족하는 하나의 타입을 지정하고 싶을 때 사용한다.
인터섹션 타입을 지정할 때는 &
을 사용하여 타입을 열거하면 된다.
type ProntEnd = { js: string };
type BackEnd = { java: string };
// ProntEnd와 BackEnd를 모두 만족하는 FullStack
type FullStack = ProntEnd & BackEnd;
값이 존재할 수도 존재하지 않을 수도 있을 경우 ?
를 사용하여 필수 요소 유무를 컨트롤할 수 있다.
function print(str1: string, str2?: string) {
console.log(str1, str2);
}
add("hello", "world!") // hello world!
add("hello") // hello
Partial<T>
: T의 프로퍼티를 선택적으로 구성
Readonly<T>
: T의 프로퍼티를 읽기 전용으로 설정하여, 값을 재할당하는 경우 에러가 발생
Record<T, K>
: 프로퍼티 키를 K, 값을 T로 하는 타입을 생성
Pick<T, K>
: T 타입 중에서 K 프로퍼티만 지정하여 타입을 생성
Omit<T, K>
: T 타입의 모든 프로퍼티 중 K를 제거하여 타입을 구성
Exclude<T, U>
: 타입 T에서 U와 겹치는 타입을 제외한 타입을 구성
Extract<T, U>
: 타입 T에서 U와 겹치는 타입만 포함하여 타입을 구성
NonNllable<T>
: T 타입에서 null과 undefined를 제외한 타입을 구성
Parameter<T>
: 함수 타입 T의 매개변수의 타입들의 튜플로 타입을 구성
ConstructorParameters<T>
: 클래스의 생성자를 비롯한 생성자 타입의 모든 매개변수 타입을 추출
ReturnType<T>
: 함수 T가 반환한 타입으로 타입을 구성
Required<T>
: 타입 T의 모든 프로퍼티가 필수로 설정된 타입을 구성
OOP
란 Object-oriented programming의 준말로 프로그래밍을 객체 단위로 나눠서 작성하는 것이다.
- 객체 지향 프로그래밍의 장점
• 프로그램을 유연하고 변경이 용이하게 만든다.
• 프로그램의 개발과 보수를 간편하게 만든다.
• 직관적인 코드 분석을 가능하게 한다.
- 객체 지향 프로그래밍의 특성: 강한 응집력(Strong Cohesion)
과 약한 결합력(Weak Coupling)
을 지향
클래스의 요소는 필드(field)
, 생성자(constructor)
, 메소드(method)
가 있으며 이 모두를 클래스의 멤버(member)
라고 부른다. 그리고 생성된 객체를 인스턴스(instance)
라고 한다.
• 클래스를 생성하기 위해서는 class
키워드를 사용한다.
• 클래스 내에서 클래스 멤버를 사용하기 위해서는 this
키워드를 사용한다.
• 클래스 인스턴스를 생성하기 위해서는 new
키워드를 생성한다.
클래스 내에 정의된 변수를 속성(property)
이라 하며, this
키워드를 이용해 접근 가능하다. 또한, 생성자(constructor)
키워드를 사용해 클래스 생성자(class constructor)를 정의할 수 있다.
class Triangle {
vertices: number; // 속성
constructor() { // 생성자
this.vertices = 3; // 속성 할당
}
}
클래스의 접근을 제한하고 싶을 때 접근 제어자를 사용한다.
public
: 어디에서나 접근 가능하며, 생략 가능한 default 값
protected
: 해당 클래스와 하위클래스에서만 접근 가능
priavte
: 클래스 외부에서 접근 불가
읽기만 가능한 속성을 선언하기 위해 readonly
키워드를 사용한다. 값을 설정한 이후에는 수정이 불가능하다.
class Triangle {
readonly vertices: number;
constructor() {
this.vertices = 3;
}
}
클래스 전체에서 공유되는 값은 정적 멤버(메서드 또는 속성)로 정의하여 사용할 수 있으며, static
키워드를 사용한다.
class Counter {
static count: number = 0;
static getCount() {
return Counter.count;
}
}
객체 지향 프로그래밍은 상속을 이용해 존재하는 클래스를 확장해 새로운 클래스를 생성할 수 있다. extends
키워드를 사용하여 클래스를 상속할 수 있으며, 파생된 클래스를 하위 클래스(subclass), 기초 클래스를 상위 클래스(superclass)라고 부른다.
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
어떤 속성에 접근할 때마다 그 값을 계산하도록 해야 하거나 내부 변수의 상태를 명시적인 함수 호출 없이 보여주고 싶을 때 getter를 사용하며, get
키워드를 붙여서 정의할 수 있다.
특정한 속성에 값이 변경될 때마다 함수를 실행할 때 setter를 사용하며, set
키워드를 붙여서 정의할 수 있다.
class Shape {
private _vertices: number = 3;
get vertices() {
console.log('Vertices getter called.');
return this._vertices;
}
set vertices(value) {
console.log('Vertices setter called.');
this._vertices = value;
}
}
추상 클래스는 인스턴스화가 불가능하다는 점에서 클래스와 다르며, 구현을 일부 포함할 수 있다는 점에서 인터페이스와 다르다.
추상 클래스나 추상 메소드를 정의할 때는 abstract 키워드를 이용한다. 추상 메소드는 클래스에 구현되어 있지 않기 때문에 파생된 클래스에서 구현해야 한다. 즉, 추상 클래스는 사용을 위해서는 상속을 강제한다.
abstract class Animal {
move(): void {
console.log("roaming the earth...");
}
abstract makeSound(): void;
}
인터페이스는 상호 간에 정의한 약속 혹은 규칙을 의미한다. 즉, 인터페이스를 통해 값이 특정한 형태를 갖도록 정의,제약할 수 있다. interface
키워드를 사용하여 정의하며, 인터페이스는 일반적으로 변수, 함수, 클래스의 타입을 체크하기 위해 사용된다.
또한, 인터페이스는 프로퍼티와 메소드를 가질 수 있다는 점에서 클래스와 유사하나 직접 인스턴스를 생성할 수 없고 모든 메소드가 추상 메소드이다. 단, 추상 클래스의 추상 메소드와 달리 abstract 키워드를 사용하지 않는다.
interface 인터페이스명 {
key: type;
}
interface User {
name: string;
age: number;
}
// 변수
const person: User = { name: 'unknown', height: 23 };
// 함수
function sayHi(user: User): void {
console.log(`Hello ${user.name}`);
}
// 클래스
class People implements User {
constructor (
public name: string
) { }
}
인터페이스에 정의되어 있는 속성을 모두 다 사용하는 것이 아닐 때는 optional을 사용하여 필수요소 유무를 정의할 수 있다. 속성 끝에 ?
를 붙이면 그 속성은 값이 존재할 수도 없을 수도 있다는 의미이다.
interface 인터페이스명 {
key?: type;
}
interface User {
name: string;
age?: number;
}
function printName(user: User) {
console.log(user.name);
}
let person = {name:'unknown'}; // age 생략해도 오류 발생 x
printName(person)
인터페이스도 마찬가지로 인터페이스를 상속할 수 있다.
주의할 점은 인터페이스끼리의 상속은 extends
키워드를 클래스의 인터페이스 상속은 implements
키워드를 사용한다.
interface User {
}
interface Developer extends User {
}
class coding implements Developer {
}
추상 클래스는 상속을 통해 추상 메소드의 구현을 강제하여 자식 클래스에서 일부 동작을 구현하도록 만든 것이다. 인터페이스는 모든 구현에 대한 계약을 작성해둔 것이다. 따라서, 추상 클래스는 선언과 구현이 모두 존재하지만 인터페이스는 선언만 존재한다. 추상 클래스는 프로그램의 전체 구조를 잡기 위해 사용하고, 인터페이스는 기본적인 설계도로써 사용한다.
제네릭(Generic)이란 데이터의 타입을 일반화한다(generalize)한다는 것을 뜻으로, 자료형을 정하지 않고 여러 타입을 사용할 수 있게 해준다.
즉, 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다. 한번의 선언으로 다양한 타입에 '재사용'이 가능하다는 장점이 있다.
// 제네릭 기본 형태
function gernericSample<T>(arg: T): T {
return arg;
}
여기서 T는 타입 변수라하며, Type의 약자로 제네릭을 선언할 때 관용적으로 사용하는 것이기 때문에 T 외에 다른 걸 적어도 무방하다.
function printArgs<T>(args: T): T {
console.log(args);
}
// 어떤 타입이 들어와도 무방
const number = [1,2,3];
printArgs(numberArray); // [1,2,3]
const text = "안녕하세요"
printArgs(textArray); // "안녕하세요"
제네릭에서 원하지 않는 속성에 접근하는 것을 막기 위해 제약조건을 사용할 수 있다. 특정 타입들로만 동작하는 제네릭에서 함수를 만들 때는 사용 extends
키워드를 사용하며, 두 객체를 비교할 때는 Keyof
키워드를 사용한다.
const printArgs = <T extends string>(args: T): T => {
console.log(args);
}
printArgs<String>("Hi");
printArgs<Number>(100); // Error
// 제약 조건을 벗어나는 타입을 선언하면 에러 발생
function getProperty<T, K extends keyof T="">(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m"); // Error
참고 자료
🔗 타입스크립트 기본 타입
🔗 활용도가 높아지는 웹 프론트엔드 언어, 타입스크립트(TypeScript)