
TypeScript라는 이름답게 정적 타입을 명시할 수 있다는 것이 순수한 자바스크립트와의 가장 큰 차이점이다. 덕분에 개발 도구(IDE나 컴파일러 등)에게 개발자가 의도한 변수나 함수 등의 목적을 더욱 명확하게 전달할 수 있고, 그렇게 전달된 정보를 기반으로 코드 자동 완성이나 잘못된 변수/함수 사용에 대한 에러 알림같은 풍부한 피드백을 받을 수 있게 되므로 순수 자바스크립트에 비해 어마어마한 생산성 향상을 꾀할 수 있다. 즉, '자바스크립트를 실제로 사용하기 전에 있을만한 타입 에러들을 미리 잡는 것' 이 타입스크립트의 사용 목적이다.
// 자동추론
const a = '5'; // string
const b = 5; // number
const c = true; // boolean
const d = undefined // undefined
const e = null // null
const x: {} = 'hello' // object가 아니다 any타입
const y: Object = 'hello' // object가 아니다 any타입
const z: object = 'hello' // 객체 타입은 이렇게 생성한다.
const f: boolean = true // 타입선언하기. 하지만, 자동으로 추론되는 타입을 굳이 써줄 필요는 없다.
배열을 선언시에는 tuple 예시를 주의깊게 보자. .push와같은 매서드를 이용한 삽입에는 타입방어가 안된다.
const arr: string[] = ['가','나','다'];
const arr2: [number, number, string] = [123,456, 'hello'];
const tuple: [string, number] = ['1',1];
tuple[2] = 'hello' // 이건 에러가 나타나지만,
tuple.push('hello'); // 이건 에러가 없다
아래와 같이 숫자와 스트링이 섞인것은 any 타입이 나올 수도, (number | string) 타입이 될 수도 있다. 따라서 정확한 타입을 원한다면, 선언을 해준다.
// number로 하였기때문에 해당 객체는 더이상 any가 아닌 number가 적용되어야한다.
const obj:{lat: number, lon: number} = {lat:1234, lon:345.4};
//선언한 타입을 변환가능하긴하다..
let aa =123;
aa = 'hello' as unknown as number; // unknown으로 바꾼 후 number로 바꾼다.
다음과같이 선언이 가능하다.
type Add = () => number; // 함수 타입=> 결과는 number
interface Minus{} // 인터페이스 아래에서 더 자세히 설명
Array<string> // 제너릭
타입스크립트에서는 함수를 타입선언, 기능선언 둘로 나눠서 해줄 수 있다
fuction add(x: number, y: number): number; // 선언만, javascript에서는 이런문구는 없다.
function add(x, y) { // 실제동작부
return x + y;
}
빈배열에는 nerver 타입이 생긴다.
const array = []; //const arr: string[] = []; 이게 맞는 표현
array.push('hello'); // 에러가 난다. never타입이므로...
위에서도 예시가있지만 또는 으로 두가지 타입이 겹처올 수있는상황에서는 !를 선언하여 null, undefined를 방지할 수 있다. (권장은 안함)
// 원래 Element | null 인데 Element로 선언이 됨, null일 수는 없다 라는 것을 뜻함.
const head = document.querySelector('#head')!; // not null
하지만 사용 권장을 안하므로 (너무 가독성이 없음 ! 모르는사람도 있을테고...)그냥 아래와 같이 구성하자.
if(head){
// 로직
}
class A{
aaa () {}
}
class B{
bbb() {}
}
//class 는 그 자체로 타입이 가능하다.
type a = A;
//그래서 변수에 선언하고 사용할때는 다음과 같이 사용한다.
const b: a = new A();
//진짜 타입만을 원할때는 typeof 를 사용한다.
const c: typeof A = A;
funtion aOrB(param: A | B){
if (param instanceof A)
pram.aaa();
else param.bbb();
}
aOrB(new A()); // class 는 인스턴트를 생성해서 선언해야한다.
type 타입은 다음과 같이 사용이 가능하다.
type World = "world";
const a: World = 'world'; // World타입이니 world string으로 밖에 입력 안된다.
type Greeting = `hello ${World}`;
const c: Greeting = 'hell'; // hello world 가 된다.
enum은 자바에서 enum이라고 생각하면 된다.
const enum Direction{
Up,
Down,
Left,
Right
}
const a = Direction.Up // 0;
const b = Direction.Down //1;
결국 위의 enum은 다음과 같다.
const Direction = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
하지만 as const 를 빼면 안됨. 빼면 타입을 추론하는 단계에서 결국
Up: number,
Down: number,
Left: number,
Right: number,
이런 뜻이 되어버림, 우리가 원하는 것은 상수로서의 의미이므로 as const를 붙힌다.
keyof는 객체 타입의 키를 타입으로 변환합니다.
typeof는 변수나 객체의 타입을 추론합니다.
const obj= {a: '123', b: 'hello', c: 'world'};
typeof obj // 타입으로 선언이 되었음. 결과는 {a: string; b: string; c: string;}
type Key = keyof typeof obj; // 그리고 이 키들을 타입으로 만들겠다. 결과는 'a'|'b'|'c'
const arr= ['123', 'hello', 'world'] ;
type Key2 = keyof typeof arr; // number | "length" | "toString" | "push" | "pop" | "concat" 등등....
또는, 그리고 를 나타내는 연산
type str = sring | null; // string or null 가능
// and연산은 타입에서 쓰기 애매하다. 그래도 객체타입에는 써보는것을 고려가능
type obj = {a: 'world'} & {b: 'hello'}
//요런식으로 가능
type Animal = {breath: true};
type Mammalia = Animal & {breed: true};
type Human = Mammalia & {think: true};
const a: Human = {breath: true, breed: true, think: true};
// 아래는 talk: true 없어서 에러
// const a: Human = {breath: true, breed: true, think: true, talk: true};
말그대로 인터페이스 java에서 사용하는 그것과 같음, 상속이 쉬움
interface A{
breath: true,
}
interface B extends A{
breed: true,
}
또한 중복해서 사용하면 추가하는 개념이 된다. 타입은 중복선언 안됨
interface A{
breath: true,
}
interface A{
breed: true,
}
void가 리턴타입으로 명시 되어있을시 매개변수나 매서드안에서 돌려주는 void타입은 사실상 어떤 것이 와도 pass한다. 단 함수 자체의 리턴 타입이 void이면 반드시 void여야한다.
function forEach(arr: number[], callback: (el: number) => void): void;
let target: number[] = [];
forEach([1,2,3], el => target.push(el)) // target.push(el) 는 void 타입이 아니다. 하지만 실행(pass)이 됨
interface Human {
talk: () => void;
} // 휴먼의 talk가 void 매서드타입이라면...
const human: Human = {
talk() {return 'abc';}
} // 다음과 같을때 string 이어도 상관없다.
function a(): void {
return null;
//void 타입의 함수에 return을 다른것을하면? 에러
}
이는 위의 예제와 같이 push와 같이 리턴타입에 상관없이 작업이 실행되어야하는 js의 자유도를 타입스크립트가 전부 막지는 못하기 때문에 허용한것.
본격적으로 타입을 가드하여 실제 사용되는 코드에 해당 타입이 아니면 실행이 안되거나 컴파일단계에서 에러가나타나게 해줄 수 있다.
interfact a {
talk: ()=> void;
}
//any의 경우 다허용이다.. 그런데 이럴거면 TypeScript 왜씀?
const b: any = a.method();
// 그래서 unknown을 사용하자. 이 경우 interface A에는 method라는 매서드가 없다. 그래서 경고를 띄워줌
const b: unknown = a.method();
타입을 여러가지 허용하는 변수에서 타입에 맞게 찾아가도록 만드는 기법
fuction numOrStr(a: number | string) {
if(typeof a === 'number')
a.toFixed(1);
}else { // 알아서 string인거 추론해준다.
a.charAt(3);
}
추가로 in연산자를 활용하는 법
type B = { type: 'b', bbb: string};
type C = { type: 'c', ccc: string};
fuction typeCheck(a: B | C){
// a.type== 'b' 이런식말고도 다음과 같이 사용가능하다.
if('bbb' in a){
a.type;
}
}
const human = { talk(); }
const dog = { bow(); }
if('talk' in a)
예측되는 Pet의 값을 Fish라고 지정한다.
그리고 로직을 이용해서 상세한 타입을 좁혀나갈 수 있다.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = getPet();
if(isFish(pet)){
pet.swim();
} else {
pet.fly();
}
읽기 기능으로만 타입선언하여 변수를 사용가능하다.
interface A{
readonly a: string;
b: string;
}
// a의 객체의 타입들을 전부 string으로 하고싶다.
type a = {[key in string]: number}
const test:a = { a: 12, b: 34, c: 432};
// a의 타입을 mapping가능하다 (mapped type)
type b = 'A'|'B'|'C'
type a = {[key in b]: b} // 뒤의 타입도 설정가능
const test:a = { A: 'C', B: 'A', C: 'B'};
타입이 섞여있고 return값을 일관되게 설정하고 싶을때는 제너릭을 이용한다.
//다음과 같은 코드가있다.
function add(x: string | number, y: string | number) {
return x + y;
}
// 내가 원하는건 다음과 같다.
add(1, 2);
add('1', '2');
// 이때 or 유니온 조건이므로 아래의 펑션도 실행이 된다. 하지만 원치않는다면?
add(1, '2');
add('1', 2);
// 해답은, 제너릭을 사용한다.
function add<T> (x: T, y: T): T {
return x + y;
}
// T는 임의의 단어이며 K, X, ABC 어떤게 와도 상관없다. 또한 상세하게 설정이 가능하다.
function add<T extends number, K extends number> (x: T, y: K): T {
return x + y;
}
// 다음의 기능도 가능하다.
<T extends {...}> // {a: string}
<T extends any[]> // string[]
<T extends (...arg: any) => any> // (a: string) => number
<T extends abstract new (...args: any) => any> // abstract new (...arg: any) => any
interface 를 만든다. interface Arr {
// forEach라는 매서드가 있고, callBack이라는 매개변수가 있는데, 이는 함수이고 () => return
// 이때 이 함수에 변수 item 은 string return 은 void타입이다. forEach의 전체 리턴값도 void이다.
forEach(callBack: (item: string) => void): void;
}
const a: Arr = [1,2,3];
a.forEach((item)=> {
console.log(item);
item.toFixed(1); // 에러발생
})
const b: Arr = [1,2,3];
b.forEach((item)=> {
console.log(item);
item.charAt(3); // 타입이 맞으므로 괜찮음
})
interface Arr<T> { // T를 사용하여 Arr의 타입을 정해준다.
forEach(callBack: (item: T) => void): void; // 마찬가지로 입력된 T와 같게 지정한다.
}
const a: Arr<number> = [1,2,3]; //이젠 타입을 지정해줘야한다.
interface Arr<T> {
forEach(callBack: (item: T) => void): void;
// T를 이용하는것은 forEach에서 사용했으니 다음과 같이 선언한다. map이므로 리턴은 배열타입으로...
map(callback: (item: T) => T): T[];
}
const a: Arr<number> = [1, 2, 3];
const b = a.map((item) => item + 1);
// 이부분에서 에러. item이 string이 아니므로 toString() 에서 에러가남
const c = a.map((item) => item.toString());
interface Arr<T> {
forEach(callBack: (item: T) => void): void;
// 위에서 결과를 보듯 들어온 값(T)과 나간 값(S)의 타입이 다르므로 다르게 설정해야한다.
map<S>(callback: (item: T) => S): S[];
}
interface Arr<T> {
forEach(callBack: (item: T) => void): void;
map<S>(callback: (item: T) => S): S[];
filter(callback: (v: T) =>v):T[];
}
const a:Arr<number | string> = [1,'2',3,'4',5];
//['2', '4'] 하지만 return type을 확인하면 number | string이다, string이 되어야함.
const d: c.filter((v)=> typeof v === 'string');
//위에서 map과 마찬가지로 변수의 타입이 결과의 타입하고 다르므로 S로 선언해야함.
interface Arr<T> {
forEach(callBack: (item: T) => void): void;
map<S>(callback: (item: T) => S): S[];
// filter는 return시 값은 그대로 돌려주므로 v를 리턴 그리고 해당 타입을 S로 변경
//하지만 T가 S로바뀔 수는 없음 따라서 앞에 extends로 확장시킴
filter<S extends T>(callback: (v: T) =>v is S):S[];
}
타입스크립트에는 타입을 커스텀 할 수 있는 다양한 유틸이 많이 있다.
특정 인터페이스나 클래스 타입을 선언시 옵셔널로 부분적으로 가져오게 할 수 있는 타입 (있어도, 없어도 상관없는 타입)
interface Profile {
name: string,
age: number,
married: boolean,
}
const a: Partial<Profile> = {
name: 'shin',
age: 44,
// 부분적으로 가저올 수 있으므로 married 생략가능
}
// 구현체
type P<T> = {
[Key in keyof T]?: T[Key]
}
const b: P<Profile> = {
married: true;
}
필수로 선언해야할때 작성
interface Profile {
name: string,
age: number,
married: boolean,
}
const a: Pick<Profile, 'name' | 'age'> ={
name: 'shin',
age: '44',
}
// 구현체
type P<T, S extends keyof T> = {
[Key in S]: T[Key];
}
const b: P<Profile, 'married'> = {
married: true;
}
//S가 T의 부모가 아니어야 통과한다.
type Ec<T,S> = T extends S ? never : T;
//S가 T의 부모야만 통과한다.
type Et<T,S> = T extends S ? T : never;
'제외'하고 필수로 선언해야할때 작성
interface Profile {
name: string,
age: number,
married: boolean,
}
const a: Omit<Profile, 'name'> ={
age: '44',
married: ture,
}
// 구현체는 Pick과 Exclude 구현체를 사용한다.
// S extends keyof any 뒤에는 key값이 와야하므로 다음과 같이 선언
type O<T, S extends keyof any> = Pick<T, Exclude<keyof T, S>>
const b: O<Profile, 'married'> = {
married: true;
}
여기까지 존재하는 유틸을 직접 만들어봤고, 그 밖에도 Required, Record, NonNullable 등 많은 유틸들이 존재한다.
참고자료들___
(참고)인프런 타입스크립트 올인원 강의