TypeScript는 정적 타입 검사기이다.
올바르지 않은 타입을 사용할 때 생기는 오류입니다.
// 'message'의 프로퍼티 'toLowerCase'에 접근한 뒤
// 이를 호출합니다
message.toLowerCase();
// 'message'를 호출합니다
message();
위에서 message가 문자면 아래 호출이 오류일것이고 함수라면 위에 toLowerCase가 오류입니다.
이와같이 잘못된 타입에 미리 오류를 호출 해주는 것이 TypeError입니다. 이러한 에러를 미리 정적으로 알려주는 것이 typescript의 큰 역할중 하나입니다. 또한 알려주는 오류를 정리하자면
또 typescript는 코딩을 하면 자동완성으로 코드를 제안해 줍니다.
물론 tsc [파일명] 을 통해서 바로 확인할 수 있습니다.
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
위의 코드를 보면 인자 2개에 각각 string, Date라고 표시해줍니다 이는 TypeScript의 명시적타입으로 사전에 해당하는 변수가 어떤 값을 가지는지 알려주어 오류를 최소화합니다.
또 명시적타입을 적는 것을 잊어버려도 똑똑한 TypeScript는 명시적타입을 추론해 알려줍니다.
또 TypeScript는 ES3라는 아주 구버전의 ECMAScript를 타겟으로 동작하는 것이 기본 동작입니다. 물론 설정을 통해서 상위 ES로 설정할 수 있지만 구버전에서의 작동은 더욱 넓은 생태계에 적용 가능 하도록 만들 수 있습니다.
먼저 원시적인 타입을 보겠습니다.
string, number, boolean
배열
그리고 타입을 모든 타입 any가 있습니다.
사용은 변수선언 시 바로 넣어줄 수 있습니다.
물론 넣어주지않아도 밑에 타입은 string으로 추론됩니다.
let myName: string = "Alice";
함수의 경우 아래처럼 반환타입을 명시할 수 있습니다.
function getFavoriteNumber(): number {
return 26;
}
객체의 경우 아래처럼 타입을 명시할 수 있습니다.
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 });
또 "?"통해 옵션으로(있어도되고 없어도되고) 타입을 선언 할 수 있습니다.
function printName(obj: { first: string; last?: string }) {
// ...
}
// 둘 다 OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
하지만 위에 last는 옵션인지입니다. 즉 함수 속에서 사용할려면 정의 여부를 체크해야합니다.
만약 string 혹은 number이라면 어떻게 하시겠습니까?
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
그럴경우 위처럼 | 를 통해 2가지타입을 가능하게 할 수 있습니다.
type Point = {
x: number;
y: number;
};
위는 타입을 직접 만들어준 형태입니다. 즉 Point타입은 number로된 x,y를 가지는 객체 타입입니다.
type UserInputSanitizedString = string; 이것처럼 별명을 만들어서 사용 가능
또한 타입들은 아래처럼 원하는 모습으로 확장가능합니다. 하지만 보는 것처럼 타입은 한번 생성되면 후에 수정할 수 없습니다.
아래의 두코드는 같은 의미입니다. 보는 것처럼 HTMLCanvasElement타입을 가진다고 알려주는 것입니다. 왜냐면 typescript와 다르게 개발자는 정확히 이 코드가 내가 사용할 때 반환하는 값을 알고있기 때문입니다.
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
아래코드에서 left, right, center은 리터럴타입입니다. 즉 유니온으로 이루어진 3개의 리터럴타입으로 alignment가 가질 수 있는 값을 제한합니다.
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
만약 strictNullChecks가 체크되어있다면(권장됨) 아래 처럼 undefined를 체크할 수 있습니다.
또 !를 통해 x가 항상 값을 가질 수 있다고 선언할 수 있고 ?를 통해 값이 없을수 있다고 선언할 수도 있습니다.
function liveDangerously(x?: number | undefined) {
// 오류 없음
console.log(x!.toFixed());
}
만약 특정 변수가 특정 타입으로 들어왔을 때는 어떤식으로 체크 할까요 바로 typeof입니다
if (typeof padding === "number")
typeof는 "string" "number" "bigint" "boolean" "symbol" "undefined" "object" "function"값을 가질 수 있습니다.
다음은 boolean의 fasle로 여겨지는 값입니다.
0, NaN, "", 0n, null, undefined
부수적으로 만약 2값이 number|string, number|object라면 두 값이 같을 경우는 두 값 모두 number일 때 뿐입니다. 이럴때는 타입이 자동적으로 number로 고정되고 이를"Equality narrowing"이라 합니다. 비슷한 경우로 특정배열의 값에 in을 사용하였다면 in을 통과한 값은 항상 배열속 값중 하나일 것이고 즉 가능한 타입이 배열 속 값으로 정해집니다.
또 x instanceof Date 처럼 특정 인스턴스인지도 확인할 수 있습니다.
하지만 이렇게 선언된 다양한 타입들에 값이 변한다면 어떻게 될까요?
위에서는 let으로 선언된a에 string혹은 number이 들어가도록 했다면 아래에서는 true나 null같은 다른 값을 넣어주면 바로 잘못되었다고 알려줍니다.
typescript는 never유형을 사용하여 존재해서는 안 되는 상태를 나타냅니다.
아래는 ()는 함수타입을 나타냅니다 즉 someArg를 인수로 boolean을 반환값으로 가지는 함수를 객체의 요소로 가지는 타입입니다.
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
아래 코드를 보면Type라는 알수없는 타입이 있습니다. 이는 무었이든 될수있다는 뜻입니다. 만약 string이라면 string[]이고 string값을 반환하는 식이 될것입니다.
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
물론 단일 타입이아닌 여러타입도 가능합니다.
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
하지만 일부 제약이 필요한 경우도 있습니다.
아래코드처럼 아무값이나 가능하지만 적어도 length는 있어서 그것을 비교 할꺼야 하고한다면 extends를 통해 제약을 줄 수 있습니다.
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
좋은 제네릭을 선언하는 방법
단순하게 보면 아래는 같은 코드로 보일 수 있습니다. 둘 다Type를 반환한다고 생각할 수 있지만
실제로 2번째 함수의 경우 반환되는 값은 any입니다
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
const a = firstElement1([1, 2, 3]);
const b = firstElement2([1, 2, 3]);
또 "fn:(arg: any, index?: number) => void"처럼 callback함수를 타입으로 표현 할수있습니다.
아래는 획기적인 방법입니다. n은 수의 첫번째를 m은 그외 나머지를 담는 배열입니다.
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
아래는 구조분해할당한 함수에 각각 타입을 설정해주는 것입니다. 실제로 자주 사용합니다.
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
아래코드는 무엇을 말할까요 JS 객체는 mutable합니다 즉 외부에서 수정한다면 그 값을 참조하는 모든 곳에서 변해버립니다. 이를 방지하기위해 readonly로 읽기 전용으로 만들 수 있습니다.
물론 readonly를 필요한 수정이 끝난 후 적용하여 안전성을 추가할 수 있습니다
interface SomeType {
readonly prop: string;
}
만약 우리가 사용하는 객체의 속성명을 정확하게 알 수 없다면 어떨까요? 하지만 그 값의 타입을 알수있다면 아래처럼 사용할 수 있습니다. 풀어 설명하자면 [index : number]는 []사이에number이 가능하고 그 반환값은 string이라는 것입니다.
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
const secondItem: string
하지만 항상 반환값이 1가지 타입은 아닐 수 있습니다.
아래의 코드를 보면 알 수 있듯 배열은 number일수도, string일수도 있습니다. 그래서 반환타입도 string|number로 되어 있구요
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
아래코드를 확인하면 문제가 없다고 생각하기 쉽상입니다. 하지만 TypeScript는 오류는 보여줍니다 왜일까요 정답은 "let mySquare = createSquare({ colour: "red", width: 100 });"에 있습니다 createSquare넣어주는 객체는 {}인데 이 속에 다른 속성이 없다는 확신을 가질 수 없습니다.(이후에 추가될 수도 있다) 그래서 정확하게 알려줘야합니다.
"let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);"
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
let mySquare = createSquare({ colour: "red", width: 100 });
물론 위에 방법으로 해결하는 것도 좋지만 객체에 다른 값이 추가되지 않을 것이라는 확신은 좋지않습니다. 언제 데이터가 변할지 모르니까요 그렇다면 아래처럼 가능성을 열어두고 개발하는 것이 좋을 것입니다.
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
아니면 이후에 확장에 대한 정확한 인터페이스를 만들어 주어도 괜찮습니다.
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
막얀 정말 객체에 대한 아무정보가 없다면 어떻게 할까요? 바로 제네릭입니다.
아래는 Box객체에 대한 제네릭선언입니다. Box는 어떠한 타입이라도 가능합니다. 물론 선언 시 타입을 지정해줘야합니다. "let box: Box"처럼
interface Box<Type> {
contents: Type;
}
아래모습은 Array제네릭입니다. 또 ReadonlyArray변경하면 안 되는 배열을 설명하는 특수 유형을 선언할 수도 있습니다.
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
아래는 튜플을 사용한 분할입니다. 배열에요소가 string, number, 나머지 boolean인것을 각각 name, version, ...input넣어 사용할 수 있습니다. API와 통신할 때 아주 유용하게 사용 가능합니다
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
keyof는 객체타입에서 객체의 속성명을 가져와 리터널 유니온을 만듭니다 a|b|c...처럼 .