타입스크립트(TypeScript)
자바스크립트(JavaScript) 기반으로 정적 타입 문법을 추가한 프로그래밍 언어
자바스크립트
동적 타입, 인터프리터 언어, 스크립팅 언어
런타임에 타입이 결정되기 때문에 런타임에 오류 발견 가능
폼 유효성 검사, 애니메이션 적용 및 이벤트 생성
타입스크립트
정적 타입, 객체 지향 컴파일 언어
코드 작성(컴파일) 단계에서 타입을 체크하여 오류 발견 가능
타입스크립트 컴파일러 또는 바벨(Babel)에 의해 자바스크립트 코드(.ts -> .js)로 변환됨
클래스 기반으로 객체 지향 프로그래밍 언어의 상속, 캡슐화, 생성자 지원
//js
function sumJs(a, b) {
return a + b;
}
//ts
function sumTs(a: number, b: number) {
return a + b;
}
타입
- string, number, boolean
- Array: string[], number[], boolean[], Array<string>, Array<number>, Array<boolean>
- Tuple: [type1, type2, ...]
- void, never
- enum: 단방향, 양방향 매핑
// js와 ts 비교
let foo = "hello";
let foo: string = "hello";
// typescript type
let isBoolean: boolean = false;
let isNull: null = null;
let isUndefined: undefined = undefined;
let isAny: any = 10;
isAny = "string"
isAny = false;
let anyList: any[] = [1, 2, "3", false];
let numList: number[] = [1, 2, 3];
let genericList: Array<number> = [1, 2, 3];
genericList.push(4);
let tuple: [string, number, number];
tuple = ["hello", 1, 2];
enum Color {RED, GREEN, BLUE}
let c1: Color = Color.RED;
console.log(c1); // 0
enum Color2 {RED = 100, GREEN = 100, BLUE}
console.log(Color2[100]); // GREEN
let c2: Color2 = Color2.BLUE;
console.log(c2); // 101
enum Os {
Window = 1 // 양방향 매핑
Ios = 100, // 양방향 매핑
Android = 'and' // 단방향 매핑
}
console.log(Os['Window']); // 1
console.log(Os[100]); // Ios
console.log(Os['and']); // undefined
인터페이스
프로퍼티를 정의하여 객체를 표현하는 방법
// object
const user: object = {
name: '홍길동',
age: 20
}
console.log(user.name); // error: Property 'name' does not exist on type 'object'.
인터페이스로 객체 정의
// interface
type Score = 'A' | 'B' | 'C' | 'F';
interface User {
name: string,
age: number,
gender?: string,
readonly birthYear: number,
[grade: number]: Score,
}
const user: User = {
name: '홍길동',
age: 20,
birthYear: 2000,
1: 'A', // Score 타입의 string 만 할당 가능
}
user.gender = 'male'; // gender 프로퍼티(string) 추가
user.birthYear = 1900; // error: readonly 속성은 객체를 생성할 때만 할당 가능
console.log(user.name); // 홍길동
console.log(user['gender']); //male
- 'gender?: string' : 다른 프로퍼티들과 다르게 존재 여부가 optional 하다. 만약 gender 프로퍼티가 존재한다면 string 타입이여야 한다.
- '[grade: number]: Score' : Index Signature
- readonly: 읽기 전용을 의미하는 키워드
인터페이스로 함수 정의
interface Add {
(num1:number, num2:number): number;
}
const add: Add = function(x, y) {
return x + y;
}
interface IsAdult {
(age:number): boolean;
}
// 람다 함수
const ia:IsAdult = (age) => {
return age > 19;
}
인터페이스로 클래스 정의
interface Car {
color: string;
wheels: number;
start(): void;
}
// Car 인터페이스 구현하여 Bmw 클래스 정의
class Bmw implements Car {
color;
wheels = 4;
constructor(color) {
this.color = color;
}
start() {
console.log('go..');
}
}
const myBmw = new Bmw('white');
console.log(myBmw);
myBmw.start();
// Car 인터페이스 상속하여 Benz 인터페이스 정의
interface Benz extends Car {
door: number;
stop(): void;
}
const myBenz: Benz = {
color: 'red',
wheels: 4,
start() {
console.log('go..');
},
door: 5,
stop() {
console.log('stop..');
},
}
myBenz.start();
myBenz.stop();
- 인터페이스는 다중 상속이 가능하다. ex) interface ToyCar extends Car, Toy { ... }
함수
function sum(x, y) { //js
return x + y;
}
function sum(x: number, y: number): number { //ts
return x + y;
}
function sayHello(): void {
console.log("hello")
}
function infiniteLoop(): never {
while(true){}
}
// 람다 함수, 화살표 함수
const multiply = (x, y) => x * y;
const multiply = (x: number, y: number): number => x * y;
Optional Parameter
function hello(name?: string):string {
return `Hello, ${name || "World"}`
}
hello(); // Hello World
hello("Kim"); //Hello Kim
- name은 optional parameter. string 타입이거나 인자로 넘어오지 않으면 undefined 이다.
function hello2(name: string, age?: number):string {
if(age !== undefined) {
return `Hello, ${name}. You are ${age}.`
}
return `Hello, ${name}`
}
hello2("Kim"); // age === undefined
hello2("Kim", 20); // age === 20
- optional parameter는 필수 파라미터의 앞에 올 수 없다.
function hello3(age: number|undefined, name: string):string {
if(age !== undefined) {
return `Hello, ${name}. You are ${age}.`
}
return `Hello, ${name}`
}
hello3(undefined, "Kim");
hello3(20, "Kim");
- hello2(name, age) -> hello3(age, name): age 의 타입을 명시적으로 undefined를 포함하는 union 타입으로 지정
Rest Parameter
function add(...nums: number[]) {
return nums.reduce((result, num) => result + num, 0);
}
add(1, 2, 3); // 6
add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 55
- 파라미터로 Rest Parameter 사용. 길이가 정해지지 않은 배열을 반환한다.
- add()는 array.reduce() 함수를 이용하여 숫자의 합을 구하는 함수이므로 파라미터의 타입은 number[] 이다.
this
interface User {
name: string;
}
const Sam: User = {
name: 'Sam'
}
// this -> any 타입
//function showName(age:number, gender:'m'|'f') {
// console.log(this.name, age, gender);
//}
// this -> User 타입
function showName(this:User, age:number, gender:'m'|'f') {
console.log(this.name, age, gender);
}
const a = showName.bind(Sam);
a(30, 'm');
- showName에서 this의 타입을 지정하지 않으면 any 타입이 된다.
- bind를 통해 showName 함수에 this를 Sam으로 고정한 함수 a를 만들었다.
Overload
interface User {
name: string;
age: number;
}
// age가 number 인지, string 인지에 따라 반환 타입이 User, string 으로 달라지는 함수
function join(name:string, age:number|string): User | string {
if (typeof age === "number") {
return {
name,
age,
};
} else {
return "나이는 숫자로 입력해주세요.";
}
}
const sam: User = join("Sam", 30); // error
const jane: string = join("Jane", "30"); // error
- join 함수가 반환하는 'string | User' 타입은 User 또는 string 타입에 할당할 수 없다. 각 타입은 서로의 타입에 할당할 수 없기 때문이다.
- 이같은 문제는 함수 오버로딩을 통해 해결할 수 있다.
// 함수 선언
function join(name:string, age:number): User;
function join(name:string, age:string): string;
// 함수 구현
function join(name:string, age:number|string): User | string {
if (typeof age === "number") {
return {
name,
age,
};
} else {
return "나이는 숫자로 입력해주세요.";
}
}
const sam: User = join("Sam", 30); // User {name: "Sam", age: 30}
const jane: string = join("Jane", "30"); // "나이는 숫자로 입력해주세요."
- 같은 이름, 다른 타입의 매개변수를 가진 함수를 정의한다.
- 실제로 구현된 함수를 호출하고 반환 타입은 위의 함수 선언부를 통해 알 수 있다.
Literal, Union/Intersection Type
String, String Literal
const a = "Hello World" // string literal
let b = "Hello World" // string
const c: string = "Hello World" // string
a: 컴파일러는 string 보다 좁은 타입(narrowed type)으로 추론한다. (Literal Narrowing)
=> 무한대의 경우의 수를 가진 string 타입이 아닌 "Hello World" 만을 허용하는 타입
b: let으로 선언되어 재할당 될 수 있으므로 string으로 추론한다.
c: 명시적으로 string 타입으로 선언하였다.
Literal Type
type HelloWorldType = "Hello World" // literal type
let a: HelloWorldType = "Hello World" // ok
a = "click" // compile error: "click" != "Hello World"
literal type 을 이용하여 let 으로 선언된 변수도 "Hello World" 만을 허용하도록 만들 수 있다.
string 타입으로 매개변수로 받을 때 발생할 수 있는 런타임 에러
function handleEvent(event: string) {}
handleEvent("click");
handleEvent("clock"); // 오타
handleEvent("hover"); // 유효하지 않은 이벤트 이름
string literal 타입을 enum 타입처럼 선언하여 사용하면 위와 같은 런타임 에러를 컴파일 시점에 확인할 수 있다.
type EventType = "mouseout" | "mouseover" | "click";
function handleEvent(event: EventType) {}
handleEvent("click");
handleEvent("hover"); // error: Argument of type '"hover"' is not assignable to parameter of type 'EventType'.
Union Type
interface Car {
name: "car";
color: string;
start(): void;
}
interface Mobile {
name: "mobile";
color: string;
call(): void;
}
function getGift(gift: Car | Mobile) {
console.log(gift.color);
if(gift.name === "car") {
gift.start();
} else {
gift.call();
}
}
- name과 color는 Car와 Mobile의 공통 프로퍼티지만 start()와 call()은 어느 한 인터페이스에만 존재한다.
- gift는 Car와 Mobile의 Union 타입이지만 공통 속성(name)을 통해 식별할 수 있다.
- Union 타입은 A 또는 B (A or B) 를 의미한다.
Intersection Type
interface Car {
name: string;
start(): void;
}
interface Toy {
name: string;
color: string;
price: number;
}
const toyCar: Toy & Car = {
name: "타요",
start() {},
color: "blue",
price: 1000,
}
- Toy와 Car의 Intersection 타입인 toyCar는 Toy와 Car의 모든 프로퍼티, 함수를 구현해야 한다.
- Intersection 타입은 A 와 B (A and B) 를 의미한다.
클래스
class Person {
private name: string; // #name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
return "Hello, " + this.name;
}
}
const person = new Person('Lee');
console.log(person.sayHello());
//위 타입스크립트를 트랜스파일링 하면 생성되는 자바스크립트 파일
const Person = /** @class **/ (function() {
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
return "Hello, " + this.name;
};
return Person;
}());
const person = new Person('Lee');
console.log(person.sayHello());
- 멤버 변수는 미리 선언해야 한다. 예외는 생성자에서 접근 제어자나 readonly 키워드를 붙여주는 방법이 있다.
// 접근 제어자
class Person {
constructor(public name: string) {
this.name = name;
}
...
}
// readonly
class Person {
constructor(readonly name: string) {
this.name = name;
}
...
}
접근 제한자 (Access modifier)
public, private, protected
- public: 자식 클래스, 클래스 인스턴스 모두 접근 가능. 접근 제한자를 명시하지 않으면 public
- protected: 자식 클래스에서 접근 가능. 클래스 인스턴스로는 접근 불가
- private: 해당 클래스 내부에서만 접근 가능. #변수명 -> private 변수
readonly
읽기 전용 키워드. 초기화만 가능. 이후에 재할당 불가
static
정적 멤버 변수나 함수로 만들어주는 키워드. 클래스명으로 접근
abstract
추상 클래스, 메서드 정의를 위한 키워드. 추상 클래스는 인스턴스를 생성할 수 없고, 상속을 통해 구현해야 한다.
추상 클래스를 상속 받은 클래스들은 메서드 선언부는 같지만 구현부는 서로 다를 수 있다.
Generics
클래스, 함수, 인터페이스의 재사용성 증대
정의할 때 타입 파라미터를 명시하고 실제로 사용할 때 타입을 지정한다.
함수에 제네릭 사용
function getSize<T>(arr: T[]): number {
return arr.length;
}
const arr1 = [1,2,3];
getSize<number>(arr1);
const arr2 = ["a", "b", "c"];
getSize<string>(arr2);
const arr3 = [false, true, true];
getSize(arr3);
// 타입스크립트가 추론한 T: boolean
const arr4 = [{}, {}, { name: "Tim"}];
getSize(arr4);
// 타입스크립트가 추론한 T: {name?: undefined;} | {name: string;}
- 함수명 뒤에 제네릭(<type parameter>)을 명시한다.
- 함수를 호출할 때 타입 파라미터(T) 자리에 들어갈 타입을 정해준다.
- arr이 배열(T[ ])이므로 T가 어떤 타입인지 상관없이 Array<T>.length를 호출할 수 있다. 따라서 함수 호출 시 타입을 명시하지 않아도 파라미터 값의 타입 추론을 통해 에러를 띄우지 않고 처리할 수 있다.
any 타입이나 union 타입으로 대체할 수 있지만 any 타입은 범위가 너무 넓고, union 타입은 가능한 모든 타입을 명시해주어야 한다.
인터페이스에 제네릭 사용
interface Mobile<T> {
name: string;
price: number;
option: T;
}
const m1: Mobile<{ color:string; coupon: boolean; }> = {
name: "s21",
price: 1000,
option: {
color: "red",
coupon: false,
}
};
const m2: Mobile<string> = {
name: "s20",
price: 900,
option: "good"
}
- 인터페이스명 뒤에 제네릭(<type parameter>)을 명시한다.
- 인터페이스 타입으로 객체를 생성할 때 타입 파라미터(T) 자리에 들어갈 타입을 정해준다.
- 이 경우 타입을 생략할 수 없고 반드시 명시해주어야 한다.
interface User {
name: string;
age: number;
}
interface Car {
name: string;
color: string;
}
interface Book {
price: number;
}
const user: User = { name: "a", age: 10 };
const car: Car = { name: "bmw", color: "red" };
const book: Book = { price: 3000 };
function showName<T>(data:T): string {
return data.name; // error: Property 'name' does not exist on type 'T'.
}
showName(user);
showName(car);
showName(book);
- T 타입 파라미터 data가 name 프로퍼티를 포함하는지 확실하지 않기 때문에 오류 발생
function showName<T extends { name: string }>(data:T): string {
return data.name;
}
showName(user);
showName(car);
showName(book); // error
- T => { name: string } 를 확장한 형태로 제한
- book은 name 프로퍼티를 포함하지 않기 때문에 오류 발생
Utility Types
keyof
interface User {
id: number;
name: string;
age: number;
gender: "m" | "f";
}
type UserKey = keyof User; // 'id' | 'name' | 'age' | 'gender'
const uk:UserKey = "name";
const invalidUk:UserKey = "address"; // error
- keyof User: User의 key들을 Union 타입으로 받는다.
Partial<T>
T 타입의 모든 프로퍼티를 optional로 하는 타입
interface User {
id: number;
name: string;
age: number;
gender: "m" | "f";
}
let user: User = { // error: User 인터페이스의 프로퍼티를 모두 정의해야 한다.
id: 1,
name: "Bob",
};
// Partial<T>
let admin: Partial<User> = {
id: 1,
name: "Bob",
address: "USA", // error: 인터페이스에 존재하지 않는 프로퍼티
}
// interface Partial<User> {
// id?: number;
// name?: string;
// age?: number;
// gender?: "m" | "f";
// }
- Partial<User>: User 인터페이스의 프로퍼티 일부만 취할 수 있다.
Required<T>
T 타입의 모든 프로퍼티를 필수로 하는 타입
interface User {
id: number;
name: string;
age?: number;
}
let admin: Required<User> = { // error: age도 필수
id: 1,
name: "Bob",
}
- Required<User>: User 인터페이스의 모든 프로퍼티를 필수로 정의해야 한다.
Readonly<T>
T 타입의 모든 프로퍼티를 readonly로 하는 타입
interface User {
id: number;
name: string;
age?: number;
}
let admin: User = {
id: 1,
name: "Bob",
}
let readonlyAdmin: Readonly<User> = {
id: 1,
name: "Bob",
}
admin.id = 4;
readonlyAdmin.id = 4; // error: 'id'는 readonly 프로퍼티이므로 할당할 수 없다.
readonlyAdmin.age = 20; // error
- Readonly<User>: User 인터페이스의 모든 프로퍼티는 readonly가 된다.
- age와 같은 optional 프로퍼티를 초기화하지 않았다고 하더라도 객체가 할당되면 추가(변경)할 수 없다.
Record<K, T>
프로퍼티 이름을 K 타입, 값을 T 타입으로 하는 타입
interface Score {
"1": "A" | "B" | "C" | "D";
"2": "A" | "B" | "C" | "D";
"3": "A" | "B" | "C" | "D";
"4": "A" | "B" | "C" | "D";
}
const score: Score = {
1: "A",
2: "C",
3: "B",
4: "B",
};
위와 같은 타입 Score를 Record<K,T>를 이용하여 다음과 같이 바꿀 수 있다.
type Grade = "1" | "2" | "3" | "4";
type Score = "A" | "B" | "C" | "D";
const score: Record<Grade, Score> = {
1: "A",
2: "C",
3: "B",
4: "F", // error: Type "F" is not assignable to type 'Score'.
5: "A", // error: '5' does not exist in type 'Record<Grade, Score>'.
};
- 프로퍼티의 이름을 Grade 로 한정하고 값을 Score 로 한정하는 타입을 만들 수 있다.
interface User {
id: number;
name: string;
age: number;
}
function isValid(user: User) {
const result: Record<keyof User, boolean> = {
id: user.id > 0,
name: user.name !== '',
age: user.age > 0,
};
return result;
}
- isValid(): User 객체의 프로퍼티 유효성을 검증하는 함
- result 의 타입? Record<keyof User, boolean>
Pick<T, K>
T 타입에서 K 프로퍼티만 골라서 정의한 타입
interface User {
id: number;
name: string;
age: number;
gender: "m" | "f";
}
const admin: Pick<User, "id" | "name"> = {
id: 0,
name: "Bob",
age: number, // error
}
// interface Pick<User, "id" | "name"> {
// id: number;
// name: string;
// }
Omit<T, K>
T 타입에서 K 프로퍼티만 생략하여 정의한 타입
interface User {
id: number;
name: string;
age: number;
gender: "m" | "f";
}
const admin: Omit<User, "id" | "name"> = {
age: 20,
gender: "m",
id: 0, // error
}
// interface Omit<User, "id" | "name"> {
// age: number;
// gender: "m" | "f";
// }
Exclude<T1, T2>
T1 타입에서 T2 타입을 제외한 타입
type T1 = string | number | boolean;
type T2 = Exclude<T1, number | string>; // boolean
type T3 = Exclude<T1, string | number | boolean>; // never
Omit vs Exclude
omit은 프로퍼티 단위로 생략, exclude는 타입 단위로 제외
NonNullable<Type>
Type에서 null과 undefined를 제외한 타입
type T1 = string | null | undefined | void;
type T2 = NonNullable<T1>; // string | void
Any
모든 타입 허용 = 타입 안정성 X
인터페이스가 존재하지 않기 때문에 컴파일러는 이 멤버에 대해 어떤 것도 알지 못한다. 따라서 타입 검사 시 무시된다.
타입 추론을 할 수 없거나 타입 체크가 필요 없는 변수에 사용한다.
기존 JS로 구현된 코드에 TS를 점진적으로 적용할 때 활용하기 좋다.
(object 타입은 인터페이스가 존재하기 때문에 any 보다 제한적)
타입스크립트에서 어떤 타입도 지정하지 않으면 컴파일러는 any 타입으로 추론한다.
any 타입 안정성 높이기
// **BAD**
function getLength(arr: any) {
return arr.length; // any
}
// **GOOD**
function getLength(arr: any[]) {
return arr.length; // number
}
배열
- 함수의 반환 타입을 any 대신 number로 추론한다. (Array<any>.length: number)
- 함수 호출될 때 매개변수가 배열인지 체크한다.
function hasTweleveLetterKey(o: {[key: string]: any}) {
for (const key in o) {
if (key.length == 12) {
console.log(key, o[key]);
return true;
}
}
return false;
}
객체
function hasTweleveLetterKey(o: object) {
for (const key in o) {
if (key.length == 12) {
console.log(key, o[key]); // ERROR
return true;
}
}
return false;
}
any 타입의 대체제
1. Union 타입: or(|) 연산자를 통해 여러 타입을 결합
function union(arg: string | number): void {
if(typeof arg === "string") {
console.log("arg is string");
} else if(typeof arg === "number") {
console.log("arg is number");
}
}
arg는 string 또는 number 타입을 가질 수 있다.
2. unknown 타입: any는 할당하는 값에 따라 타입이 변하는데 비해, unknown은 타입이 그대로 unknown이다.
never
'타입'이 가능한 값의 집합이라면 never 타입은 값의 공집합
집합에 어떤 값도 없기 때문에 never 타입 변수는 any 타입을 포함하여 어떤 값도 가질 수 없다.
never 타입 변수에는 never 타입만 할당할 수 있다.
let any_type: any;
const never_type: never = any_type; // error. 'any' 타입은 'never' 타입에 할당할 수 없다.
// 함수의 반환 값이 never 타입 이기 때문에 오류가 발생하지 않음
const never_type: never = (function():never { throw new Error('ERROR') })();
never 타입의 사용
never 는 일반적으로 함수의 리턴 타입으로 사용된다.
이때 항상 오류를 출력하거나 리턴 값을 절대로 내보내지 않음을 의미한다.
이는 무한 루프에 빠지는 것과 같다.
// 항상 오류 발생
function invalid(message:string): never {
throw new Error(message);
}
// never 타입을 결과 추론(Inferred)
function fail() {
return invalid('실패');
}
// 무한 루프
function infiniteAnimate(): never {
while ( true ) { infiniteAnimate(); }
}
'Advance II > TypeScript' 카테고리의 다른 글
| [TS] Todo List & Ts Sorter 개발 (0) | 2023.09.20 |
|---|---|
| [TS] Index Signature (0) | 2023.09.12 |