반대로 말하면 TS에서 모든 타입이 any인게 JS
**주의!!
TS는 JS로 변환되에야 하는게 메인 룰이므로 타입을 지웠을 때도 말이 되는 JS코드여야 한다. 헷갈리면 : 뒤에부터 순차적으로 지워보자
ex)
// 변수에 타입지정
let a: number = 5;
/** 매개변수와 리턴값에 타입지정
* 1. 일반함수일때는 :뒤에 return 타입 지정
* 2. 화살표함수일때는 => 뒤에 return 타입 지정
*/
function add(a: number, b: number): number {
return a + b;
}
const add2: (a: number, b: number) => number = (a, b) => a + b;
type Add = (a: number, b: number) => number;
const add3: Add = (a, b) => a + b;
interface Add2 {
(x: number, y: number): number;
}
const add4: Add2 = (a, b) => a + b;
/** 베열과 객체 타입 지정
* 1. 배열의 타입은 대괄화, 꺽세(제네릭) 두가지 방법으로 표현 가능
* 2. 객체의 타입은 이름 바로 뒤에 지정
*/
const arr1: string[] = ["123", "456"];
const arr2: number[] = [123, 456];
const arr3: Array<number> = [123, 456];
const arr4: [number, number, string] = [1, 2, "3"]; // 튜플(길이가 정해져있는 array)
const obj: { name: string; age: number } = { name: "jaehun", age: 27 };
TS는 멍청한듯 똑똑해서 타입추론을 잘해준다.
예를 들어 아래 코드 같은경우 const는 상수이므로 바뀔일이 없어 string 타입이 아닌 값자체를 타입으로 지정해버릴수 있고 let은 변수이므로 string 타입으로 추론을 해준다.
긁어부스럼 만들지 말고 TS를 믿되 이시끼가 any타입으로 지정하거나 내가 원하는 타입과 다른 타입을 추론하면 타입을 지정해주자
TS에서 타입을 빼도 올바른 JS코드가 되도록 짜야하고 타입 지정 자리를 잘 기억하자
TSC에서 JS로 변환할떄 interface, type, 타입지정한 것들은 다 사라진다.
크게 타입 지정방식은 4가지 있다.
추가적으로 타입만 미리 만들 수 있다.
ex)
function addFnc(a: number, b: number): number;
function addFnc(a: number, b: number): number {
return a + b;
}
이런 식으로 지정 가능
또한 강제로 타입을 변환해주는 as도 JS로 만들 때 사라진다.
정리하면
**주의!
빈배열을 쓸 때 조심하자 ⇒ 빈배열은 never타입이기 때문에 배열의 타입도 지정해줘야 한다
무조건 존재한다에 내 전재산 걸 수 있으면 !를 뒤에 붙여준다. (근데 비추,,, 인생 몰라~)
!는 위험하다 그래서 안정한 방식을 바꿔주는게 좋다
TS는 오타도 잡아주고 불안정한 실수도 잡아주니깐 매우 고맙다ㅎ
String과 string은 다르다. String은 new Array
처럼 new String
으로 string을 만들어 줄때 쓰는 애다
그니깐 속 편하게 쓰지말자 대문자는 원시 레퍼 타입이 아니다.
type으로 지정해줄 수도 있고 그 타입을 템플릿 리터럴 안에 넣을 수도 있다 그러면 정교한 자동완성을 도와준다.
동일하다 그냥 rest파라미터를 사용할 뿐
function rest(a: number, ...args: string[]) {
console.log(a, args); // 1, "2", "3"
}
rest(1, "2", "3");
TS가 만능은 아니라 다 막아주지는 못한다 (ex. tuple인데 push 못 막아줌)
const tuple: [string, number] = ['1', 1];
tuple[2] = 'hello'; //error
tuple.push('hello');
enum은 여러개의 변수들을 하나의 그룹을 묶어서 제한할때 사용하는데 자주 안씀..
근데enum안쓰면 타입이 아주 복잡해짐,,,
객체로 사용하는 방법이 keyof와 typeof를 사용해야하는데
객체의 키값의 타입을 가져오고 싶을 때 keyof를 사용하고 변수를 타입으로 사용하고 싶을 때 typeof를 사용한다
따라서 객체의 키값을 타입으로 사용하고 싶다면 typeof, keyof를 이용해야한다.
typeof만 썼을 때 객체를 타입으로 바꿔주고
keyof까지 쓰면 객체의 key갑들을 타입으로 바꿔줌
이렇게 하면 객체의 key값들만 가져와서 타입으로 사용한다
응용하면
type value = typeof ODirection[keyof typeof ODirection]
을 사용해서 vlaue값들만 가져와서 타입으로 사용할 수 도 있다.
그냥 객체에 넣어써 쓰는게 더 편함
두개의 차이점은 TSC 컴파일을 거쳤을 때 enum은 남아있지 않고 객체는 JS코드로 남아있다.
tip)
변수에 그냥 할당해주면 원시타입으로 타입추론을 하지만
내가 상수로 쓰고 싶다! 하면 변수에 타입을 할당해주는 방법도 있지만
이렇게 as로 const를 매핑해주면 원큐에 해결된다.
간단하게 타입을 지정하고 싶으면 type을 쓰고 객체지향으로 사용하고 싶다 하면 interface를 쓰자
또는 관계이기 때문에 마음대로 넣을 수 있지만 타입추론이 이상해질 위험이 있다.
obj에서 사용할 수 있으며 타입끼리의 and역할을 한다 |는 (or역할!)
// &는 객체의 요소를 모두 포함해야한다는 뜻이다.
type A = {
a: string;
};
type B = {
b: string;
};
// &는 A, B 타입 모두 포함해야함(and)
const aa: A & B = { a: "hello", b: "world" };
// |는 A, B 타입 중 하나만 포함하면 됨(or)
const bb1: A | B = { a: "hello" };
const bb2: A | B = { b: "world" };
const bb3: A | B = { a: "hello", b: "world" };
TIP)
TS를 사용할 때 처음 타임을 잘 정해야 한다. 처음이 꼬이면 그 뒤에가 다꼬인다,,,
type과 &를 이용해서도 상속을 표현할 수 있지만 (거의 확장에 가까움)
원래 쓰던 extends로 상속받는게 더 익숙하고 편리하잖아?
간단하게 쓰고 싶으면 type, 객체지향으로 쓰고 싶으면 interface
근데 대부분 interface 쓸꺼임ㅎ
또한 interface는 같은 이름으로 여러번 선언할 수 있다. 똑같은 이름으로 선언하면 이전 interface에 추가가된다.
interface AA {
breath: true;
}
interface AA {
plus: () => void;
}
const plusType: AA = {
breath: true,
plus: () => {},
};
이렇게 해서 라이브러리를 분석할 때 내가원하는 타입을 추가해 줄 수 있다.
TIP)
type지정시 네이밍룰
요즘에는 2번으로 가는 추세 IDE가 알아서 다 해준다ㅋㅋㅋ
타입을 집합으로 생각하면서 뭐가 더 큰 개념의 타입인지 생각하자 (어떤 타입이 어떤 타입안에 포함되어있는지를 고려해야 함)
Rule : 좁은 타입에서 넓은 타입으로는 대입이 가능하고 그 반대는 불가능하다
객체는 속성이 적을 수록 넓은타입이고, 속성이많을수록(상세할수록) 좁은타입이다.
type Aa = { name: string }; // 넓은 타입
type Bb = { age: number }; // 넓은 타입
type C = { name: string; age: number }; // 좁은타입
type small = Aa & Bb;
type big = Aa | Bb;
// 좁은 타입을 넓은 타입에는 대입 가능
const test_1: small = { name: "jaehun", age: 27 };
const test_2: big = test_1;
// 넓은 타입을 좁은 타입에는 대입 불가능
const test_3: big = { name: "jaehun", age: 27 };
const test_4: small = test_3;
예외의 경우가 있는데 객체에 새로운 속성을 넣어줬을 경우(더 상세하므로 더 좁은 타입임)은 안된다.
왜냐하면 객체의 리터럴은 잉여속성검사를 하기 때문에 알려진 속성만 지정할 수 있기 때문이다
이를 해결하는 방법이 중간에 값을 따로 빼주는 것이다.
const obj_test5 = { name: "jaehun", age: 27, married: false };
const test_5: small = obj_test5;
이렇게하면 가능하다
이게 가능한 이유는 객체 타입 속성을 검사하는 것을 초과 프로퍼티 검사(Excess Property Checks)라고 하는데 객체 리터럴을 변수에 직접 할당하거나 인수로 전달 될 때는 초과 프로퍼티 검사를 받는다 그래서 대상 타입(미리 지정한 타입)이 갖고있지 않은 프로퍼티를 갖고 있으면 error가 난다.
하지만,
객체를 다른 변수에 할당하게 되면 변수 타입을 지정해놓지 않았기 때문에 초과프로퍼티 검사를 받지않는다.
그렇게 초과 프로퍼티 검사를 회피해서 할당해줄 수 있다.
//error
const test_5: small = { name: "jaehun", age: 27, married: false };
//success
const obj_test6 = { name: "jaehun", age: 27, married: false };
const test_6: small = obj_test6;
그렇다고 대상 타입의 프로퍼티가 빠져있는 값을 받아줄만큼 멍청하지는 않다
//error
const obj_test7 = { alias: "hun", age: 27, married: false };
const test_7: small = obj_test7;
참고
TS에서 void 타입은 3가지의 경우를 가지고있다.
void를 두가지 의미로 받아들여야 한다. return ‘값이 없다’는 뜻과 return 값을 ‘사용하지 않겠다’ 로 쓰인다.
함수의 return 타입이 void인 경우는 return 값이 없다는 뜻으로 return값을 만들어주면 error가 나오고
메개변수가 return 타입이 void인 경우, 메서드의 return 타입이 void인 경우는 return 값을 사용하지 않겠다는 뜻으로 return에 어떠한 값이 들어가도 error는 나지 않는다. return 값이 뭐든간에 사용하지 않겠다는 뜻이다.
TIP1)
JAVA에 interface처럼 구현물없이 타입만 지정하고 싶을 때 declare
를 사용할 수 있다.
declare const a: string;
declare function a(x: number): number;
declare class A {}
주로 사용하는 용도가 다른 script에서 구현된 type을 사용하고 싶은데 내가 작성하는 파일 내에는 없으니깐 ts가 오류는 뱉어내서 타입만 복사해와서 끌어와 쓰는 느낌이다.
TIP2)
강제로 형변환 하는 두가지 방법
as 원하는타입
<원하는타입>
근데 TS에서는 이렇게 형변환하는것을 실수로 보기때문에 error를 낸다
그래서 자신있으면
as unknown as 원하는타입
으로 형변환할 수 있다.
any쓰는 순간 그 뒤에 코드는 TS가 타입체킹을 포기해 버린다.
unknown은 지금당장 타임을 모르겠고 나중에 쓸때 지정해 줘야한다
const testObj: { talk: () => void } = {
talk() {
return 3;
},
};
const anyType: any = testObj.talk();
anyType.talk(); // 타입지정 포기
const unKnownType: unknown = testObj.talk();
(unKnownType as { talk: () => void }).talk(); // 사용할 때 타입 지정해줘야 함
unKnown을 가장 많이 보는 경우가 try catch 문에서 본다 error를 처리할때 본다.
| 같은 거로 넓은 타입을 사용할 때 TS는 모든 가능성을 체크하기 때문에 error가 나온다
이럴 때 타입을 좁혀주는 타입가드가 필요하다
typeof 연산자로 타입 좁히기
하지만 typeof는 string, number, boolean, undefined, symbol, object, function 으로 구별하기 때문에 array나 obj들은 구별을 할 수 없다.
Array같은 경우는 Array.isArray를 통해 타입좁하기
Array.isArray()를통해 배열인지 아닌지 체크해준다.
Class끼리 구별하는 방법은?
Class는 그 Class로 만들어낸 인스턴스 자체가 type이다.( new A)
이럴때 Type을 체크하는 방법은 검사하는 값 instanceof 클래스이름
로 인스턴스 여부를 확인해서 구별한다.
Obj끼리 구별하는 방법은?
Obj는 두가지 방법으로 구별할 수 있다.
속성값으로 구별하기
구별하고자 하는 obj가 모두 같은 속성을 가지고 있다면 속성값으로 구별할 수 있다.
// Object
type B = { type: "b"; bbb: string };
type C = { type: "c"; ccc: string };
type D = { type: "d"; ddd: string };
type A = B | C | D;
function typeCheck(a: A) {
if (a.type === "b") {
a.bbb;
} else if (a.type === "c") {
a.ccc;
} else {
a.ddd;
}
}
속성 유무로 구별하기
구별하고자 하는 obj들의 차이점을 찾아야 한다.
속성값이 동일하다면 다른 속성을 가지고 있는지 찾아서 구별할 수 있다.
// Object
type B = { type: "b"; bbb: string };
type C = { type: "c"; ccc: string };
type D = { type: "c"; ddd: string };
type A = B | C | D;
function typeCheck(a: A) {
if (a.type === "b") {
a.bbb;
} else if ('ccc' in a) {
a.ccc;
} else {
a.ddd;
}
}
TIP)
객체는 서로 다른 걸 구별하기 좀 더 복잡하기 때문에 여러개 객체를 만들때 type속성을 만들어서 넣어주는게 편하다
위의 방법으로 알기 너무 힘들정보로 타입이 복잡해지면
리턴값에 is를 사용해서 타입 가드에 해당하는 로직을 따로 함수로 빼서 커스텀 함수로 타입을 구분해 줄 수 있다.
interface Cat {
meow: number;
}
interface Dog {
bow: number;
}
// 타입가드를 위한 커스텀 함수
function catOrDog(a: Cat | Dog): a is Dog {
if ((a as Cat).meow) {
return false;
}
return true;
}
const cat: Cat | Dog = { meow: 3 };
if (catOrDog(cat)) {
console.log(cat.meow);
}
if ("meow" in cat) {
console.log(cat.meow);
}
이런식으로 커스텀 함수를 만들어서 if문의 조건식으로 사용해줄 수 있다.
(is가 없으면 타입을 구분하지 못한다)
실전적인 예제로 JS에서 비동기처리를 하는 Promise를 사용할 때
// Promise 성공했을 때
const isFulfilled = <T>(
input: PromiseSettledResult<T>
): input is PromiseFulfilledResult<T> => input.status === "fulfilled";
// Promise 실패했을 때
const isRejected = (
input: PromiseSettledResult<unknown>
): input is PromiseRejectedResult => input.status === "rejected";
// Promise가 실행되면 Settled 상태가 되는데 Settled 상태안에서 성공했을 경우인 fulfilled 상태와 실패했을 경우인 rejected 상태로 나눠진다.
const promises = await Promise.allSettled([
Promise.resolve("a"),
Promise.resolve("b"),
]);
// 커스텀 타입가드 함수를 이용해서 성공했을 때 타입을 PromiseFulfilledResult로 지정해준다.
const success = promises.filter(isFulfilled);
// 커스텀 타입가드 함수를 이용해서 실패했을 때 타입을 PromiseRejectedResult로 지정해준다.
const errors = promises.filter(isRejected);
export {};
이렇게 타입을 나눠줄 수 있다.
TS에서 {}와 대문자 Object 타입은 객체를 뜻하는게 아니라 null, undefined를 제외한 모든 타입을 의미한다
정확히 object를 하고 싶으면 소문자 object를 쓰면되는데 이 방법은 지양하고 interface로 처리해주자
readonly 타입을 넣어주면 읽기만 되고 변경이 안된다.
interface readOnly {
a: string;
readonly b: string;
}
const readOnlyTest: readOnly = {
a: "hi",
b: "world",
};
readOnlyTest.a = "change";
readOnlyTest.b = "change"; // error - TS2540: Cannot assign to 'b' because it is a read-only property.
여러가지 속성이 타입이 모두 같다면 index signature 문법을 사용해서 간편하게 쓸 수 있다.
type IndexSignature = {
[key: string]: number;
};
const indexSignatureTest: IndexSignature = {
a: 1,
b: 2,
c: 3,
};
여기서 key, value에 들어갈 값들에 Mapped Type를 사용해서 지정해줄 수 있다.
type Key = "jo" | "jae" | "hun";
type IndexSignature2 = {
[key in Key]: number;
};
const indexSignatureTest2: IndexSignature2 = {
jo: 1,
jae: 2,
hun: 3,
};
Cf)
interface에서는 |, & 가 안되기 때문에 이런거 쓸 때는 type으로 써야함
class 자체의 타입은 typeof이고 class의 이름은 class로 만들어낸 인스턴스를 가리킨다
JS에는 없는 3가지가 TS에 추가되었다. (객체지향을 사용하기 위해)
interface Interface {
a: string;
b: number;
}
class TSClass implements Interface {
private a: string = "test";
protected b: number = 1;
c: string = "기본값이 public";
method() {
console.log(this.a);
console.log(this.b);
console.log(this.c);
}
}
class inheritClass extends TSClass {
method() {
console.log(this.a); // error
console.log(this.b);
console.log(this.c);
}
}
new inheritClass().a; // error
new inheritClass().b; // error
new inheritClass().c;
근데 이렇게 하면 type error가 난다 interface에 a는 public이지만 class의 a 는 private이기 때문이다.
하지만 interface에는 private을 쓸 수 없다.
따라서 아래와 같은 방법으로 error를 해결 할 수있다.
interface Interface {
readonly a: string;
b: number;
}
class TSClass implements Interface {
private readonly _a: string = "init";
get a() {
return this._a;
}
protected _b: number = 1;
get b() {
return this._b;
}
set b(v: number) {
this._b = v;
}
c: string = "기본값이 public";
method() {
console.log(this._a);
console.log(this._b);
console.log(this.c);
}
}
class inheritClass extends TSClass {
method() {
console.log(this._a); // error
console.log(this.a); // 가능..
console.log(this._b);
console.log(this.b);
console.log(this.c);
}
}
new inheritClass()._a; // error
new inheritClass().a; // 가능...
new inheritClass()._b; // error
new inheritClass().b; // 가능..
new inheritClass().c;
⇒ 미해결
하지만 이렇게 하면 밖에서도 a,b호출이 가능해진다;;;
⇒ 해결
interface는 특성 자체가 public이기때문에 interface안에 private, protected를 할 이유가 없다.
따라서 private, protected의 기능을 완벽하게 사용하기 위해서는 interface를 사용하지 말자…
또 다른 방법으로는 abstact class(추상클래스)를 사용하는 방법이 있다.
근데 객체지향의 원칙을 중시 안한다면 그냥 implements를 빼버리고 class안에 바로 타입을 지정해버리는게 가장 편하긴 하다
Cf) abstract class
추상클래스(abstract class)는 그 자체로는 사용하지 못하고 추상 클래스를 상속받은 클래스(구현체)가 있어야 한다 추상 클래스 안에서도 메서드를 만들 수 있고, 추상메서드는 추상 클래스 내에서 구현 할 수 없고 반드시 상속받은 클래스에서 구현되어야 한다
// 추상 클래스
abstract class AbstractClass {
private readonly a: string = "init";
protected b: number = 1;
c: string = "기본값이 public";
abstract method(a: string): void;
method2() {
console.log(this.a);
console.log(this.b);
console.log(this.c);
}
}
class realClass extends AbstractClass {
method(a: string) {
console.log(this.a); //error
console.log(this.b);
console.log(this.c);
}
}
class inheritAbstractClass extends realClass {
method() {
console.log(this.a); // error
console.log(this.b);
console.log(this.c);
}
}
new inheritAbstractClass().a; // error
new inheritAbstractClass().b; // error
new inheritAbstractClass().c;
optional type(?.)은 그냥 생각하는 그대로임
//optonal type
function abc(a: number, b: number, c?: number) {}
abc(1);
abc(1, 2);
abc(1, 3, 4);
generic이 중요한데 아직은 타입을 모르는데 사용할때 타입을 가져와서 쓰겠다 할때 제네릭을 쓸 수 있다.
예를들어 1+1 =2, “1”+”1” = “11” 을 해주는 함수를 만들고싶을 때
/** 내가 만들고 싶은 함수
* genericExercise(1,2) => 12
* genericExercise("1","2") => "12"
*/
function genericExercise<T extends number | string>(a: T, b: T): T {
return a + b;
}
genericExercise(1, 2); // 3
genericExercise<number>(1, 2); // 3
genericExercise("1", "2"); // "12"
genericExercise<string>("1", "2"); // "12"
genericExercise(1, "2"); // error
제네릭의 선언위치는 기본적으로 식별자 뒤이고, 화살표 함수는 ()앞이다.
/** 제네릭의 선언위피는 기본값은 식별자 뒤, 화살표함수는 ()앞이다
* function a<T>() {}
* class B<T>() {}
* interface C<T> {}
* type D<T> = {};
* const e = <T>() => {};
*/
이때 제네릭의 타입을 제한하는 방법은
// <T extends {...}> // 특정 객체
// <T extends any[]> // 모든 배열
// <T extends (...args: any) => any> // 모든 함수
// <T extends abstract new (...args: any) => any> // 생성자 타입
// <T extends keyof any> // string | number | symbol
이런 방법이 있다.
// 기본값은 값 뒤에 타입 지정
const a = (b: number = 3, c: { myName: string } = { myName: "hun" }) => {
return "3";
};
// 화살표 함수의 제네릭은 함수 앞에
// react에서 JSX,TSX쓸때는 제네릭을 쓸때 기본값을 써줘야함
const add = <T extends unknown>(x: T, y: T) => ({ x, y });