[220322_TIL] typescript를 배워보자 | Everyday Types
전체 코드는 git 에서 확인 가능합니다. 작성된 내용은 ts 공식문서를 학습하며 주관적으로 재구성한 내용으로, 오류가 있을 수 있습니다. 잘못된 내용은 댓글로 알려주시면 감사히 수정하겠습니다 : )
변수 타입
ts에서의 타입은 (당연하게도) js의 타입과 많이 닮아있다. number
, string
, boolean
은 타입의 선언 방식 또한 동일하다. 또한 const, var, 또는 let 등을 사용하여 변수를 선언할 때, 변수의 타입을 명시적으로 지정하기 위하여 타입 표기를 추가할 수 있으며 이는 선택 사항이다.
배열은 두가지 방법으로 타입을 설정할 수 있다. Array<number>
과 number[]
는 완전히 동일하다.
// 1.1 변수에 대한 타입 표기 - 원시타입(선택사항)
const name: string = "jisu"
const age: number = 10
const toggle: boolean = true
// 1.2 Array
const numArray1: Array<number> = [1, 2, 3]
const numArray2: number[] = [4, 5, 6]
const strArray1: Array<string> = ["a", "b", "c"]
const strArray2: string[] = ["e", "f", 'g']
// 1.3 any
let obj: any = {x: 0}
obj.foo() // any 지정해서 오류 안 생김
obj()
obj.bar = 100
obj = "hello"
const n: number = obj;
주의할 점으로, any
를 사용하면, ts는 개발자가 알아서 오류없이 작성할 것이라고 믿고 타입 검사를 하지 않는다. 그래서 런타임에서 오류가 난다. 되도록이면 any
대신 정확한 타입을 써야한다.
함수 타입
함수에서는 정의할 타입은 크게 input
, ouput
이 있다. 즉, 파라미터와 반환값의 타입을 지정할 수 있다.
function addFive(x: number): number {
return x + 5
}
위와 같이 파라미터와 반환값에 대한 타입을 명시할 수 있다. 이렇게 작성하고 나면 함수를 호출할 때 명시한 타입과 다르면 경고메시지를 보여준다. 참으로 친절하다!
익명 함수
forEach
의 파라미터로 들어가는 익명함수에 대해서, 파라미터 s
에 대한 타입을 지정하지 않아도 string
이라는 것을 알아채고, 경고메시지를 준다.
const names = ["Alice", "Bob", "Eve"];
names.forEach((s) => {
console.log(s.toUppercase()) // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
})
이는 ts가 문맥적 타입 부여
를 한 것으로, 타입을 지정하지 않아도 ts가 알아서 판단한 것이다.
객체 타입
js에서 가장 많이 사용되고 중요한 객체 타입이다.
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
point를 의미하는 pt 객체가 있고, pt의 속성인 x, y가 number
인지 확인한다. 프로퍼티 사이에는 ;
를 사용해도 되고 , ,
콤마를 사용해도 된다. 만약 타입을 지정하지 않으면, any
로 간주된다.
옵셔널 프로퍼티
아래 함수는 인자를 선택적으로 받는 경우에 사용할 수 있다. 객체의 프로퍼티 뒤에 ?
를 붙여 선택적 프로퍼티라는 것을 의미한다. 있어도 되고, 없어도 문제 없는 것이다. 다만, 선택 사항이기 때문에, 없는 경우에도 문제가 되지 않게 분기처리해야 한다.
function printName(obj: { first: string; last?: string }) {
console.log(obj.last.toUpperCase()); // 오류 - `obj.last`의 값이 제공되지 않는다면 프로그램이 멈추게 됩니다!
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// 최신 JavaScript 문법을 사용하였을 때 또 다른 안전한 코드
console.log(obj.last?.toUpperCase());
}
printName({ first: "Bob" }); // OK
printName({ first: "Alice", last: "Alisson" }); // OK
유니언 타입
유니언은 합집합, or 을 의미한다. 두가지 이상의 타입을 허용하는 방법이다.
다음의 함수는 id라는 파라미터를 받는데, 이때 id는 number
이거나 string
이다.
그래서 위 두 타입이 아닌, 다른 타입을 입력했을 때는 오류가 발생한다.
function printId(id: number | string) {
console.log("Your ID is: " + id); // num, str 둘다 오류 없는 작업
console.log(id.toUpperCase()); // num 일때 오류 발생
}
printId(101); // OK
printId("202"); // OK
printId({ myID: 22342 }); // obj 여서 오류
그리고 둘 이상의 타입을 받을 때는, 함수 안에서 공통적으로 작동할 수 없는 동작에 대해서는 분기처리를 할 필요가 있다.
유니언 좁히기
위에서 number
, string
을 둘을 허용하고, 함수 내부적으로 타입 검사를 통해 서로 다른 결과값을 출력할 수 있다. 이때 파라미터 타입이 정확히 number 또는 string 이므로, 조건문은 if-else
면 충분하다.
function printId2(id: number | string) {
if (typeof id === "string") {
// 이 분기에서 id는 'string' 타입을 가집니다
console.log(id.toUpperCase());
} else {
// 여기에서 id는 'number' 타입을 가집니다
console.log(id);
}
}
또 다른 방법으로, 배열을 받는 경우 isArray()
등으로 확인 할 수도 있다.
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// 여기에서 'x'는 'string[]' 타입입니다
console.log("Hello, " + x.join(" and "));
} else {
// 여기에서 'x'는 'string' 타입입니다
console.log("Welcome lone traveler " + x);
}
}
공통적 특성을 가지는 유니언
다음 함수는 첫 3개를 반환하는 함수로, 배열 또는 문자열을 입력 받는다. 둘다 이터러블한 객체이기때문에 slice()
메서드를 가지므로, 분기할 필요가 없다. 반환타입은 number[] | string
이다.
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
타입 별칭
여러 함수에 동일한 파라미터가 사용되는 경우, 중복을 최소화 하기 위해 타입별칭을 사용할 수 있다.
type Point = {
x: number;
y: number;
};
function printCoord2(pt: Point) { // 위의 타입으로 간편하게 사용
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord2({ x: 100, y: 100 });
자주 사용되는 객체 타입을 타입 별칭
으로 선언하여 사용 할 수 있다.
유니온 타입 별칭
타입 별칭을 그저 자주 사용하는 타입을 사용하기 좋게 선언할 뿐이다. 그래서 다음과 같이 유니언도 따로 이름을 부여할 수 있다.
type ID = number | string;
인터페이스
인터페이스는 타입과 비슷한 듯 다른데, 가장 큰 차이점인 인터페이스는 object
의 타입만 정의한다는 것이다.
위에서 타입 별칭으로 작성된 것을 interface로 동일하게 작성할 수 있다.
interface MyPoint {
x: number;
y: number;
}
function printCoord3(pt: MyPoint) { // 파라미터의 구조에만 관심, 타입 검사
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord3({ x: 100, y: 100 });
인터페이스 vs 타입 별칭
ts 공식 문서에서 설명하는 차이점 예시이다.
많은 경우 타입 별칭보다는 인터페이스를 많이 사용하는 것 같다.
타입 단언
ts가 제안하는 타입보다 더 정확하고 자세한 타입을 명시하고 싶을 때 사용한다.
둘 다 동일한 방법으로, as
를 사용하거나 꺽쇠로 앞에 명시할 수 있다. 개발자가 element의 id를 정확하게 입력했고, 그러한 객체가 canvas하는 것을 정확히 알고 있을 때 사용하는 것이다. 기본적으로 document.getElementById
의 타입은 HTMLElement
이다.
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
리터럴 타입
문자와 숫자의 경우 오직 몇개의 값만 허용하고 싶은 경우가 있다. 이때 일반적으로 변수를 선언할 때를 생각해보면 const
를 사용하면, 값을 수정할 수 없는데 이를 활용한 방법이다.
let changingString = "Hello World";
changingString = "Olá Mundo";
// 변수 `changingString`은 어떤 문자열이든 모두 나타낼 수 있으며,
// 이는 TypeScript의 타입 시스템에서 문자열 타입 변수를 다루는 방식과 동일합니다.
changingString;
let changingString: string
const constantString = "Hello World";
// 변수 `constantString`은 오직 단 한 종류의 문자열만 나타낼 수 있으며,
// 이는 리터럴 타입의 표현 방식입니다.
constantString;
변수를 선언할 때 let
을 사용하면 해당 변수의 타입은 그의 상위 타입, 즉 위 경우에는 string
으로 추론된다. 반면 const
를 사용하면, 선언했던 그 값으로 정확히 타입이 지정된다.
let
을 사용하고 리터럴 타입을 지정하고 싶을 때는 다음과 같이 작성할 수 있다.
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
함수에서의 사용
리터럴 타입은 함수등에서 사용될 때 진가가 발휘된다. 다음과 같이 미리 지정된 몇개의 옵션만 적용하기 위해 리터럴 타입을 사용할 수 있고, 유니온으로 작성한 목록들 외에 다른 값이 들어오면 경고를 보여준다.
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre"); // 오타 감지
파라미터 뿐만 아니라 함수의 반환값에서도 사용 가능하다.
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
리터럴 타입의 유니온이 아닌, 다른 type 과 같이 사용하는 것도 가능하다.
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
리터럴 추론
js에서 객체는 const로 선언하여도, 프로퍼티를 수정하거나 추가 할 수 있다. 그렇기 때문에 let 처럼 동작하며, 리터럴 추론이 원하는대로 동작하지 않을 수 있다.
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method); // "GET" 이라는 리터럴이 아닌, 문자열로 추론되어 오류 발생
handleRequest
함수는 url
과 method
두 개의 파라미터를 입력 받는다. 이때 method
는 리터럴 타입으로 오직 GET과 POST만 입력받고자 한다.
그런데 req 객체는 각 프로퍼티가 위 함수의 파라미터와 정확히 일치하는 것 같지만, 위에서 말한 것 처럼 오브젝트의 속성을 수정가능한 let
처럼 간주되어 리터럴이 아닌 문자열로 추론되어 오류가 발생한다.
이를 해결하기 위해 몇가지 방법이 존재한다.
해결 방법1. 타입 단언 추가
as
를 사용하여 보다 자세하고 정확한 타입을 명시하는 방법이다.
// 수정 1:
const req2 = { url: "https://example.com", method: "GET" as "GET" };
// 수정 2
handleRequest(req2.url, req2.method as "GET");
해결 방법2. as const로 객체 전체를 리터럴로 변경
객체를 수정 불가한 const로 변환하는 것이다.
const req3 = { url: "https://example.com", method: "GET" } as const;
handleRequest(req3.url, req3.method);
null
과 undefined
strictNullChecks
옵션을 설정하지 않으면, 타입을 지정하더라도 null, undefied가 허용된다. 따라서 보다 엄격한 타입 지정을 위해 위 옵션을 켜고 사용하는 것이 좋다.
strictNullChecks
를 설정한 경우 유니온 타입을 사용한 후 조건 분기를 통해 오류를 방지할 수 있다.
function doSomething(x: string | undefined) {
if (x === undefined) {
// 아무 것도 하지 않는다
} else {
console.log("Hello, " + x.toUpperCase());
}
}
또한 null 이 아니라는 것이 명확한 경우에는 null 이 아님을 단언하는 !
를 사용하여 다음과 같이 작성할 수 도 있다.
function liveDangerously(x?: number | undefined) {
// 오류 없음
console.log(x!.toFixed());
}
Reference
Author And Source
이 문제에 관하여([220322_TIL] typescript를 배워보자 | Everyday Types), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@parkjisu6239/220322TIL-typescript를-배워보자-Everyday-Types저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)