JavaScript 에서 열거형 대신 객체 형태로 나타내지만, TypeScript 에서는 문자형 열거형과 숫자형 열거형을 지원하며, 특정 값(상수)의 집합을 정의할 때 사용된다.
(auto-incrementing) : 값을 지정하지 않는다면 자동적으로 0부터 시작하여 1씩 증가하게 값이 지정된다.
enum Color{
Red, //0
Green, //1
Blue. //2
}
let c: Color; //enum 타입 변수 선언
c = Color.Green; //enum 타입 변수에 값 할당
c = 'Hello'; //Error : enum 타입변수로 지정했기 때문에 설정된 enum 값인 Red, Green, Blue 만 올수 없음
let d: Color.Red; //enum 값을 타입 자체로도 사용할 수가 있다.
enum Week {
Sun, //0
Mon = 22,
Tue, //23
Wed, //24
Thu, //25
Fri, //26
Sat //27
}
console.log(Week.Mon); // 22
console.log(Week.Tue); // 23
enum Week {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat
}
console.log(Week.Sun); // 0
console.log(Week['Sun']); // 0
console.log(Week[0]); // 'Sun'
let weekName: string = Week[0];
console.log(weekName); // 값이 0인 일요일이 출력 -> Sun
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue',
}
console.log(Color.Red); // red
console.log(Color['Green']); // green
자바에서 클래스 위주로 인터페이스를 다루지만, 타입스크립트에서는 객체를 위주로 다룬다. 인터페이스는 객체의 껍데기로써 변수, 함수, 클래스에 사용 가능하다.
interface
예약어를 사용하여 인터페이스를 생성한다.//예약어 interface + 대문자 네이밍 컨벤션 으로 인터페이스 생성
interface Human{
name: string;
age: number;
boo(): void;
}
//인터페이스 자체를 타입으로 줘서 객체 생성
const person: Human = {
name: 'Kane',
age: 30,
boo: () => conso.log('He is in M.U.');
};
콜론:
앞에 ?
를 붙이면 옵션 속성이 된다.지정한 타입 | undefined
선언이나 마찬가지이다.interface CraftBeer{
name: string;
hope?: number; //hope 속성은 명시해도 되고 안해도 된다.
}
function brewBeer(beer: CraftBeer){
console.log(beer.name);
}
let myBeer = { name: 'Saporo' }; //hope 속성 없이 name 속성만 설정
brewBeer(myBeer); //Saporo
myBeer.hope = 5; // 선택적 프로퍼티에 의해서 나중에 속성값을 넣어줄 수 있다.
//매개변수에서 인터페이스를 타입으로 받을 수 있다.
function booboo(a: Human): void{
console.log(`${a.name} is ${a.age} years old`);
};
booboo(person); // Kane is 30 years old
person.boo(); // He is in M.U.
interface Login {
//함수명이 아닌 함수의 모양(인자, 리턴) 타입을 쓴다.
(name: string, password: string) : boolean;
}
let loginUser: Login = function(id, pw) {
//타입추론에 의해 선언할 함수에 타입을 굳이 쓸 필요가 없다.
console.log('로그인 했습니다.');
return true;
}
console.log(loginUser('jw', '123'));//로그인 했습니다. true
loginUser('jw', '123'); //로그인 했습니다.
implements
키워드를 사용해 클래스 정의 옆에 붙여주면 된다.interface IUser{
name: string;
getName(): string;
}
class User implements IUser{
name: string;
constructor(name: string){
this.name = name;
}
getName(){
return this.name;
}
}
const neo = new User('Neo');
neo.getName(); // Neo
extends
키워드를 사용한다.as
를 이용해 객체를 생성할 수 있다.interface Person {
name: string;
age: number;
}
interface Devloper extends Person{
language: string;
}
const person: Developer = {
name: 'John',
age: 25,
language: 'JAVA',
}
interface Programmer{
favoriteProgrammingLanguage: string;
}
interface Korean extends Person, Programmer{
// 두 개의 인터페이스를 받아 확장
isLiveInSeoul: boolean;
}
const person: Korean = {
name: '홍길동',
age: 33,
favoriteProgrammingLanguage: 'kor',
isLiveInSeoul: true,
}
// as 이용시 프로퍼티를 직접 명명해야 한다.
const person = {} as Korean;
person.name = '홍길동';
person.age = 33;
person.favoriteProgrammingLanguage: 'kor',
person.isLiveInSeoul: true,
타입의 새로운 이름을 만드는 것으로 기존의 타입을 참조하는 것을 의미한다.
type
키워드를 사용한다.type MyString = string;
let str1: string = 'hello';
let str2: MyString = 'Hello world';
//string 타입으로 사용하고 MyString === string 동일한 의미를 갖는다.
// 리터럴 객체 타입
const a1: {
name: string;
age: number;
talk: () => void;
} = {
name: '홍길동',
age: 12,
talk(){},
};
//type alias 객체 타입
type Ty = {
name: string;
age: number;
talk: () => void;
};
const a2: Ty = {
name: '홍길동',
age: 12,
talk(){},
};
// 인터페이스 객체 타입
interface In {
name: string;
age: number;
talk: () => void;
}
const a3: In = {
name: '홍길동',
age: 12,
talk() {},
};
type Person = {
name: string;
age: number;
}
interface User {
name: string;
age: number;
}
//type -> type 확장 불가
type Students extends Person {
classNmae: string;
}
//interface -> interface 확장 가능
interface Students extends User{
className: string;
}
//type -> interface 확장 가능
interface Students extends Person{
className: string;
}
타입스크립트이 정적타입을 지원하는 언어라 타입추론 기능 특징을 가진다.
타입 추론이란 개발자가 굳이 변수 선언할 때 타입을 쓰지 않아도 컴파일이 스스로 판단해서 타입을 넣어주는 것을 말한다.
자바스크립트와 더불어 타입스크립트는 객체 지향 프로그래밍이므로 클래스를 사용할 수 있다.
class Animal{
name: string;
contructor(name: string){
this.name = name;
}
}
extends
키워드를 사용하여 상속받아 확장할 수 있다. class Cat extends Animal{
//수퍼 클래스 Animal 을 상속함으로써
//부모 클래스의 인스턴스인 name 과 생성자 메서드는 작성하지 않아도 접근하여 사용가능하다.
//추가 메서드를 작성시 부모 클래스의 인스턴스를 가져올 수 있다.
getName(): string{
return `Cat name is ${this.name}.`;
}
}
let cat: Cat = new Cat('Lucy');
console.log(cat.getName()); //Cat name is Lucy.
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
leg: number = 0;
// @Override Animal
constructor(name: string) {
super(name);
}
// @Override Animal
move(distanceInMeters = 5) { // 오버라이드 되서 굳이 또 매개변수 타입을 선언 할 필요 없다.
super.move(distanceInMeters);
}
poison() {
console.log('shoot poison !!');
}
}
class Horse extends Animal {
leg: number = 4;
// @Override Animal
constructor(name: string) {
super(name);
}
run() {
console.log('start Run !!');
}
}
Animal 부모 클래스를 extends 키워드를 사용하여 자식 클래스 Snake, Horse 가 상속받았다. 상속된 클래스의 생성자 함수는 부모 클래스의 생성자를 실행하기 위해 super()
를 호출해야한다.
자식 클래스 Snake 에서 move() 메서드를 Override 하였다. 이는 기존의 메서드를 덮어씌우고 기존 함수를 개조하는 개념이다.
let sam: Snake = new Snake('Sammy the Python'); // 자식 클래스 생성자로 객체 생성
sam.leg; // 자식 클래스의 인스턴스 변수 : 0
sam.move(); // 오버라이드 한 부모클래스의 메소드 : 'Sammy the Python moved 5m' (5만 변경)
sam.poison(); // 자식 클래스의 메소드 : 'shoot poison !!'
let tom2: Horse = new Horse('Tommy the Palomino');
tom2.leg; // 자식 클래스의 인스턴스 변수 : 4
tom2.move(34); // 오버라이드 한 부모클래스의 메소드 : 'Tommy the Palomino moved 34m'
tom2.run(); // 자식 클래스의 메소드 : 'start Run !!'
let tom: Animal = new Horse('Tommy the Palomino');
tom.leg; //Animal 형식에 leg 속성이 없습니다.
tom.move(34);
tom.run(); //Animal 형식에 run 속성이 없습니다.
tom 변수의 타입을 Animal로 지정하고 자식 클래스인 Horse 생성자를 받았다.
부모 클래스로 타입형을 선언하고 자식클래스를 생성해서 할당하였기 때문에, 부모 클래스의 인스턴스인 name과 move()만 사용할 수 있고, Horse의 인스턴스인 leg와 run() 를 사용할 수 없어 이런 에러가 발생하였다.
접근 제어자는 클랫, 메서드 및 기타 멤버의 접근 가능성을 설정하는 객체 지향 언어의 키워드이다.
class Animal {
public name: string;
public constructor(theName: string) {
this.name = theName;
}
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
// 둘이 완전히 같은 구조이다.
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Animal {
// private 수식어 사용
private name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat extends Animal {
getName(): string {
return `Cat name is ${this.name}.`; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'
}
}
let cat = new Cat('Lucy');
console.log(cat.getName());
console.log(cat.name); // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
cat.name = 'Tiger'; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
console.log(cat.getName());
Animal 을 상속받은 서브 클래스 Cat 은 수퍼 클래스에서 private 으로 선언한 name 인스턴스에 접근하지 못하기 때문에 오류가 발생한다. name 은 오직 Animal 클래스만 사용가능하고 다른 클래스에서는 사용할 수 없다.
클래스 내 인스턴스는 오직 읽기 전용으로 명시하기 위해서는 readOnly
키워드를 사용한다. 변경해서 안될 프로퍼티 값을 클래스 내 선언 시에 작성해준다. 추후에는 그 인스턴스는 다른 값으로 할당될 수 없다.
class Animal {
readonly name: string; // 읽기 전용 속성
constructor(n: string) {
this.name = n; // 읽기 전용은 초기화 할때만 값 대입 가능
}
}
let dog = new Animal('Charlie');
console.log(dog.name); // Charlie
// 그러나 초기화가 아닌 추후에 접근해서 할당은 불가능
dog.name = 'Tiger'; // Error - TS2540: Cannot assign to 'name' because it is a read-only property.
프로그래밍을 할 때 변수는 변할 수 있는 것, 상수는 항상 고정된 값을 말한다. number
, string[]
과 같이 타입은 항상 고정되어 절대 변하지 않는 타입을 사용해오고 있다. 유연성을 위해 number | string | undefined
과 같이 |
를 사용해 union 타입을 사용하기도 했다.
하지만 항상 고정되어 의도대로 흘러가지는 않는다. 변수로 언제든지 변할 수 있는 타입(타입을 변수화)을 통해 보다 유연성있고 코드 재사용성을 높이고 안정성을 보장할 수 있는 바로 제네릭 기능이다.
function add(x: string|number , y: string|number): string | number {
return x + y;
}
add(1,2); //3
add('hello', 'world'); //helloworld
add(1,'2');
add('1', '2');
유니언을 통해 매개변수 x, y 는 string 혹은 number 의 타입을 모두 받을 수 있다. 하지만, x: string, y: number
혹은 x: number, y: string
도 가능하지만 컴파일러는 이를 에러로 감별한다.
우리는 이것을 각각 타입을 분리하여 오버로딩을 할 수 있다.
function add(x: string, y: string): string ;
function add(x: number, y: number): number;
function add(x: any, y: any){
return x+y;
}
add(1,2); //3
add('hello', 'world'); //helloworld
add(1,'2'); // 호출 x
add('1', '2'); // 호출 x
밑의 두 함수는 전달인자가 아예 받아들여지지 않기 때문에 함수 호출이 일어날 수가 없다.
하지만 타입별로 구현해야하는 함수는 코드가 길어지게 되고 가독성이 떨어지는 단점이 있어 이런 한계를 극복하기 위해 제네릭이 사용된다.
//함수형
function add<T>(x:T, y:T): T{
return x+y;
}
//화살표 함수형
const add2 = <T>(x:T, y:T): T => { ... }
add<number>(1,2); //3
add<string>('hello', 'world'); //helloworld
꺽쇠<>
기호를 변수명, 함수명 앞에 쓰면 타입 단언이 되게 된다.
변수명을 뜻하는 문자 T
를 이용하여 제네릭을 표현한다.
하지만 코드가 복잡해서 컴파일러가 간혹 멍청하게 타입 추론을 잘못한다면 직접 제네릭을 선언해야하는 경우가 있다.
인수를 배열로 받을 경우는 제네릭 처리를 T[]
나 Array<T>
해주어야 한다.
function loggingIdentity<T>(arg: T[]): T[]{
console.log(arg.length);
return arg;
}
function loggingIdentity2<T>(arg: Array<T>): Array<T>{
console.log(arg.length);
return arg;
}
//제네릭 인터페이스
interface Mobile<T>{
name: string;
price: number;
option: T; // 제네릭 타입
}
// 제네릭 자체에 리터럴 객테 타입도 할당 할 수 있다.
const m1: Mobile<{color: string; coupon: boolean}> = {
name: 's21',
price: 1000,
option: { color: 'read', coupon: false},
// 제네릭 타입의 의해서 option 속성이 유연하게 타입이 할당됨
};
const m2: Mobile<string> = {
name: 's20',
price: 900,
option: 'good', // 제네릭 타입의 의해서 option 속성이 유연하게 타입이 할당됨
};
Mobile 인터페이스를 사용하여 만든 객체는 option의 값으로 어떤 타입이 들어갈지만 작성해주면 굳이 여러 개를 만들지 않고도 m1이나 m2 처럼 재사용할 수 있다.
type TG<T> = T[] | T;
const number_arr: TG<number> = [1,2,3,4,5];
const number_arr2: TG<number> = 12345;
const string_arr: TG<string> = ['1', '2', '3', '4', '5'];
const string_arr2: TG<string> = '12345';
type alias 과 제네릭을 같이 사용할 수 있다.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(v: T, cb: (x: T, y: T) => T) {
this.zeroValue = v;
this.add = cb;
}
}
let myGenericNumber = new GenericNumber<number>(0, (x, y) => {
return x + y;
});
let myGenericString = new GenericNumber<string>('0', (x, y) => {
return x + y;
});
myGenericNumber.zeroValue; // 0
myGenericNumber.add(1, 2); // 3
myGenericString.zeroValue; // '0'
myGenericString.add('hello ', 'world'); // 'hello world'
클래스도 인터페이스처럼 생성자 함수로 타입을 참조해야한다.
제네릭은 사용하는 시점에 타입을 결정해줌으로써 사실상 아무 타입이나 집어넣어도 상관없다.
function identity<T>(p1: T): T {
return p1;
}
identity(1);
identity('a');
identity(true);
identity([]);
identity({});
입력값에 대한 유연성은 확보했지만, 각 함수에 대해 사용처에 따라 입력값을 제한 할 필요가 생긴다.
이를 위해 extends
키워드를 사용해 적용되는 타입의 종류를 제한할 수 있는 기능을 제공한다.
클래스의 extends 는 상속하여 확장의 의미를 가지지만, 제네릭에서의 extends 는 제한의 의미를 가진다.
<T extends K>
는 T가 K에 할당 가능해야 한다
를 의미한다.
type numOrStr = number | string;
// 제네릭에 적용될 타입에 number | string 만 허용
function identity<T extends numOrStr>(p1: T): T {
return p1;
}
identity(1);
identity('a');
identity(true); //! ERROR
identity([]); //! ERROR
identity({}); //! ERROR
T는 number | string 에만 할당 가능하기 때문에 boolean, [], {} 에는 허용되지 않는다.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
T에는 .length 프로퍼티가 없어서 오류가 발생한다. 개발자 입장에서는 길이를 구하고자 하는 로직을 짜기 위해 length 를 사용하겠지만 컴파일러 입장에서는 타입을 알지 알지 못하기 때문에 충분히 에러가 발생한다.
그래서 타입 가드 를 통해 분기하는 방법도 있지만, 해당 속성을 포함하도록 지정해주어야 한다.
interface Lengthwise{
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
//제네릭 T는 반드시 {length: number} 프로퍼티 타입을 포함해야 한다.
//arr에서 .length 프로퍼티가 존재함을 알기 때문에 접근가능하다.
console.log(arg.length);
return arg;
}
keyof
키워드를 통해 객체의 key 값만 뽑아 접근할 수 있다.
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'); // 오류: 인수의 타입 'm' 은 'a' | 'b' | 'c' | 'd'에 해당되지 않음.
원래 자바언어를 썼어서 그런지 자바스크립트를 쓸 때 타입을 구분하지 않아서 생소했는데, 타입스크립트에서는 원래 알던 대로 타입을 지정할 수 있어서 큰 어려움은 없었다.
타입 추론 기능이나 제네릭 등은 아직 생소하여서 예제나 과제를 통해 더 많이 접해야 할 필요가 있는 것 같다.