인프런 타입스크립트 기초를 통해 공부중이며 제로초님의 유료강좌입니다.
코드를 통으로 가져오지는 않겠습니다.(내가 글을 쓰면서 복습하는게 목적이기때문에 필요한 부분만)
타입스크립트에 익숙하지않다면 자바스크립트로 먼저 코드를 작성하고 그 후에 타입스크립트로 변환하도록하자.
타입스크립트 (Typescript)
인터페이스(interface)란?
"
인터페이스는 변수나 함수, 클래스와 같은 객체에서 사용한다.
여러가지 타입을 갖는 프로퍼티로 이루어지며 새로운 타입을 정의할 수 있다.
인터페이스에 선언된 프로퍼티나 메소드를 통해 구현을 강제할 수 있으며 이것을 통해 일관성을 유지할 수 있도록 한다. 인터페이스는 클래스와 유사하나 직접 인스턴스를 생성할 수 없다. 또한 모든 메소드는 추상 메소드이다.
"
인터페이스로 변환하는 예제를 확인해보자.
const test = {
a : 1,
b : 2
} as const;
// interface
interface TEST{
a : 1,
b : 2
}
이때 as const로 선언한 test와 interface로 선언한 test가 같은지를 생각해봐야한다.
as const로 선언 후 마우스를 포커싱해보면 vscode에서 readonly라고 뜨는것을 확인할 수 있다.
붙이지 않아도 값을 고정할 수 있지만 as const와 동일한 결과가 아닌 예제를 보자.
interface TEST{
a : string
b : string
}
interface TEST{
readonly a : string
b : string
}
위의 예제처럼 상수 값이 아닌 경우를 넣게될 경우에 as const와는 똑같다고할 수가 없다.
이때 readonly 키워드를 붙여주어 as const와 동일하게 만들어줄 수 있다.
interface TEST{
readonly a : 1,
readonly b : 2
}
인터페이스의 줄바꿈은 콤마, 세미콜론, 아무것도 붙이지 않는 것이 가능하다.
// 콤마
interface TEST{
a : 1,
b : 2
}
// 세미콜론
interface TEST{
a : 1;
b : 2;
}
// 줄바꿈
interface TEST{
a : 1
b : 2
}
인터페이스의 특징으로는 상속이 있다.
상속받을 경우 extends를 사용하면된다.
interface TEST{
readonly a : string
b : string
}
// 이렇게 하면 TEST를 상속받아 오는 것
interface Example extends TEST{
// TEST의 a, b 타입을 가지고 있다.
}
// TEST를 상속받은 Example을 통해 타입 선언
const Hi : Example = {
// TEST a, b 타입을 적용
};
인터페이스 객체의 틀을 잡고 상속하는 점이 클래스와 비슷하다고 할 수 있다.
또한 같은 이름의 인터페이스를 여러개 생성할 수 있다.
이럴 경우 인터페이스들은 하나로 합쳐진것과 동일하다.
타인이 만든 라이브러리의 인터페이스에 문제가 생겼을때 수정을 하기 위함이거나 확장을 해야할 경우에 사용한다.
interface TEST {}
// ....
interface TEST{}
type aliases는 타입으로 선언하는 것이다.
타입은 인터페이스처럼 같은 네이밍으로 선언했다고 해서 합쳐지는 것이 아니고 여러개 선언시에 에러가 난다.
type Test = {};
인터페이스와 타입 에일리어스의 선언 방식의 차이
interface TEST {};
// '=' 이 들어가야한다.
type TEST = {};
타입이 더 넓은 범위, 인터페이스는 객체일때 많이쓴다고 한다.
타입도 물론 인터페이스를 대체하여 객체를 만들수도 있다고 한다. (하지만 결국 선호도의 차이일 수도 있다고했다.)
타입으로 커스텀 타입을 생성할 수 있다.
새로운 test라는 타입을 생성하였다. test의 타입은 string 또는 number다.
type test = string | number
type을 객체로 사용하는 예시
type test = {
a : 'a'
} | string;
// string값, 객체 둘 다 사용가능하다.
const example : test = 'hello';
const example : test = {
a : 'a'
};
객체로 사용할때에는 인터페이스를 사용하는게 좋다는데, 타입은 나중에 복잡해질 경우 사용하게 된다고 한다.
(위에 말했듯 선호도 일수도 있다고 했다.)
유니언이 필요할 경우에 타입을 쓴다.
type test = {
//..
} | string;
keyof
위에서 선언한 key, value 값을 아래에서 타입으로 사용하기 위해서는 keyof를 통해 쓸 수 있다.
key 예시
interface TEST {
readonly orange : 'orange',
readonly apple : 'apple'
}
// TEST와 같은 키를 return 받는 함수.
function get(o) : keyof TEST{
// ...
}
반대로 value 값 예시
interface TEST {
readonly orange : 'orange',
readonly apple : 'apple'
}
// 인자값 o가 TEST의 value 값과 동일해야할때
function get(o ㅣ TEST[keyof TEST]) : keyof TEST{
// ...
}
만약 불가피하게 어떠한 값이 들어올 지 모르게되는 경우에는 넓은 범위로 선언해준다.
하지만 엄격하게 사용하는 것이 좋기때문에 지양하는 것이 좋다.
// [key : string] : number
interface Example{
a : 2,
b : 3,
[key : string] : number;
}
const example : Example = {
a : 2,
b : 3,
c : 100
}
lib.es5.d.ts파일을 확인해보면 Object.keys()는 반환값이 string[]으로 고정되어있다.
따라서 에러가 생기는 경우가있다.
이때는 타입스크립트가 스스로 범위를 좁게 잡지 생기지 못하기때문에 강제로 넓은 범위로 넓혀주는 것이 좋다.
function TEST (o : Example[keyof Example]) : keyof Example {
// as ['a', 'b', 'c'] 강제로 선언
return (Object.keys(test) as ['a', 'b', 'c']).find((k) => test[k] === o);
}
또한 위의 코드에서 find메서드를 사용했는데, 이 메서드는 ES6에서 사용할 수 있으므로 tsconfig.json을 수정해줘야한다.
{
"compilerOptions" : {
"strict" : true,
// ES5, DOM이 기본값이므로 그 이상의 값을 다 입력해주자.
// 이 코드는 아직까지 줄이기가 힘들다고 했다.
"lib" : ["ES5", "ES6", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "DOM"]
},
"exclude" : ["*.js"]
}
또한 명령어 입력 시 tsconfig.json을 포함할 경우에 해당 파일이 무시되어 에러가나는 경우가 생기는데
이때 파일을 입력하지 않은채로 명령어를 입력해준다.
// cmd
npx tsc -w
find의 타입을 확인해보면 리턴 값이 T | undefined로 고정되어있는 것을 확인할 ㅜㅅ 있다.
이때는 아래와 같이 느낌표를 추가하여 해결할 수 있다.
느낌표를 입력하면 에러가 사라지는데, 느낌표는 타입시스템에서는 오류라고 말하지만 개발자가 그 코드를 보증하는 것이다. 이건 프로그래머의 재량이다. 느낌표가 없어서 실행이 안되는 경우들이 존재하는데 이때 사용할 수 있다. undefined나 null을 보증할 수 있다. (하지만 안쓰는 방법도 존재한다.)
function TEST (o : Example[keyof Example]) : keyof Example {
return (Object.keys(test) as ['a', 'b', 'c']).find((k) => test[k] === o)!;
}
this에러
화살표 함수가 아닐 경우에 발생하는 경우가 있는데, 이때 자바스크립트와 가장 크게 달라진다.
매개변수 첫번째 위치에 this 와 this의 타입을 선언해준다.
document.querySelectAll('button').forEach((btn) => {
btn.addEventListener('click', function(this : HTMLButtonElement, e : Event){
const my = this.textContent;
});
});
타입스크립트는 html을 모르기 때문에 this.textContent가 string | null로 유추한다
lib.dom.d.ts파일을 보면 textContent에서 확인 할 수 있다.
타입스크립트가 스스로 추론하지 못하는 값들을 선언해줘야한다.
document.querySelectAll('button').forEach((btn) => {
btn.addEventListener('click', function(this : HTMLButtonElement, e : Event){
// TEST의 key값과 동일할 경우 as를 통해 타입 선언을 해준다.
const my = this.textContent as keyof TEST;
});
});
느낌표, if문, 꼼수를 통해 에러 보증하기
querySelector도 html을 인식하지 못하기 때문에 자바스크립트처럼 사용하면 에러가나게된다.
이때 lib.dom.d.ts파일에서 E | null로 유추하고 있음을 확인할 수 있다.
느낌표를 사용하여 에러를 막을 수 있다.
document.querySelector('#test')!.style.background = '';
또한 if문으로도 해결할 수 있다.
if문으로 감싸줄 경우 null이 아님을 보증하기 때문에 에러를 방지할 수 있다.
if(document.querySelector('#test')){
document.querySelector('#test').style.background = '';
}
또 다른 방법으로 꼼수를 쓸 수도 있다.
근데 이렇게 하느니 느낌표를 쓰라더라.
if(!test){
throw new Error('nope')
}
return test;
그 후 querySelector뒤에 style에서 에러가 또 발생하는데, querySelector는 E | null 이었다.
이때 E는 Element를 상속받는데, E는 style이 존재하지 않는다고 뜬다.
이때 제네릭으로 해결할 수 있는데, (아직 안배웠다)
현재 할 수 있는 방법으로는 as를 통해 형변환을 하는 방법이다.
HTMLDivElement는 style이 존재하고 Element는 style이 없기때문에 HTMLDivElement로 형변환을 해주자.
if(document.querySelector('#test')){
document.querySelector('#test') as HTMLDivElement.style.background = '';
}
아직배우지는 않았지만 제네릭으로 썼을때는 아래와 같이 사용할 수 있다.
또한 변수에 담지않을 경우 문자열이 같다고 인식하지 않기때문에 변수에 담아 중복코드를 방지해주자.
// (X)
if(document.querySelector<HTMLDivElement>('#test')){
document.querySelector<HTMLDivElement>('#test').style.background = '';
}
// (O)
const test = document.querySelector<HTMLDivElement>('#test')
if(test){
document.querySelector<HTMLDivElement>('#test').style.background = '';
}
타입스크립트의 엄격함때문에 형변환을 해야하는 경우도 있다.
textContent는 string이기때문에 아래와 같이 형변환을 해줘야한다.
(document.querySelector('#test') as HTMLDivElement).textContent = String(point);