

원시 타입(Primitive Type)은 동시에 한개의 값만 저장할 수 있는 타입
배열이나 객체 같은 비 원시타입들은 동시에 여러개의 값들을 저장할 수 있음 / 반면 원시타입은 숫자면 숫자, 문자열이면 문자열 딱 하나의 값만 저장할 수 있음
let num1: number = 123;
let num2: number = -123;
let num3: number = 0.123;
let num4: number = -0.123;
let num5: number = Infinity;
let num6: number = -Infinity;
let num7: number = NaN;
이때 number 타입으로 정의한 변수에는 number 타입을 제외한 값을 할당할 수 없다.
number 타입의 값이 사용할 수 없는 toUpperCase 등의 메서드는 사용할 수 없다.
let str1: string = "hello";
let str2: string = 'hello';
let str3: string = `hello`;
let str4: string = `hello ${str1}
// boolean
let bool1 : boolean = true;
let bool2 : boolean = false;
boolean 타입은 참과 거짓만을 저장하는 타입입니다. true 또는 false만 이 타입에 해당됩니다.
let null1: null = null;
let unde1: undefined = undefined;
let numA = null;
위와 같이 아직 값이 미정인 변수에 임의로 null을 할당하는 경우가 있는데, 타입스크립트에서는 numA의 타입이 number라고 지정되어 있으면 에러가 난다.
let numA: number = null; // ❌
하지만 만약 이렇게 쓰고 싶다면 추후 나오는 유니온 타입이나 tsconfig.json의 strcitNullChecks(엄격한 null 검사) 옵션을 false로 설정하면 된다.
{
"compilerOptions": {
"strictNullChecks": false,
},
}
하지만 strictNullChecks은 strict 옵션의 하위 옵션이기때문에,
strict 옵션이 true이면 자동으로 true로 설정되며 반대로 strict 옵션이 false이면 자동으로 false로 설정된다. 만약 위 코드처럼 우리가 직접 설정하면 설정된 옵션에 따른다.
타입스크립트에는 string, number 처럼 범용적으로 많은 값을 포함하는 타입 뿐만 아니라 딱 하나의 값만 포함하는 타입도 존재한다. 따라서 다음과 같이 변수의 타입을 숫자 10으로 설정하는 것 또한 가능하다.
let numA: 10 = 10;
let strA: "hello" = "hello";
let boolA: true = true;
let boolB: false = false;
타입스크립트에서만 제공되는 특별한 타입으로 타입 검사를 받지 않는 특수한 치트키 타입이다.
만약 number의 값을 할당받았다가 string을 할당받아야하는 케이스에서는 any라는 치트키를 쓸 수 있다.
let anyVar: any = 10;
anyVar = "hello";
anyVar = true;
anyVar = {};
anyVar.toUpperCase();
anyVar.toFixed();
anyVar.a;
이렇듯 any 타입은 어떠한 타입 검사도 받지 않기 때문에 아무 타입의 값이나 범용적으로 담아 사용할 수 있고 또 다양한 타입의 메서드도 마음대로 호출해서 사용해도 문제가 되지 않는다.
또 any 타입의 값은 어떤 타입으로 정의된 변수던 문제 없이 다 할당할 수 있다. 따라서 다음과 같이 number 타입의 변수 num에 any 타입의 값 anyVar를 할당해도 문제가 발생하지 않는다.
하지만 그렇기때문에 최대한 사용하면 안된다!
위에 코드에서는 런타임에러가 나지만 타입스크립트의 컴파일과정에서는 에러를 찾을 수 없기때문이다.
unknown 타입은 any 타입과 비슷하지만 보다 안전한 타입이다. unknown 타입의 변수는 어떤 타입의 값이든 다 저장할 수 있다.
let unknownVar: unknown;
unknownVar = "";
unknownVar = 1;
unknownVar = () => {};
하지만 any와 다르게 unknown 타입의 값은 어떤 타입의 변수에도 저장할 수 없다. 또한 어떠한 연산에도 참여할 수 없다.
let num: number = 10;
(...)
let unknownVar: unknown;
unknownVar = "";
unknownVar = 1;
unknownVar = () => {};
num = unknownVar; // ❌
unknownVar * 2 // ❌
정리하자면 unknown 타입은 독특하게도 변수의 타입으로 정의되면 모든 값을 할당받을 수 있게 되지만,
반대로 unknown 타입의 값은 그 어떤 타입의 변수에도 할당할 수 없고, 모든 연산에 참가할 수 없게 된다. 쉽게 정리하면 오직 값을 저장하는 행위밖에 할 수 없게 된다.
만약 위와 같이 unknown 타입의 값을 number 타입의 값처럼 취급하고 곱셈 연산을 수행하게 하고 싶다면 다음과 같이 조건문을 이용해 이 값이 number 타입의 값임을 보장해줘야 한다.
if (typeof unknownVar === "number") {
// 이 조건이 참이된다면 unknownVar는 number 타입으로 볼 수 있음
unknownVar * 2;
}
따라서 특정 변수가 당장 어떤 값을 받게 될 지 모른다면 any 타입으로 정의하는 것 보단 unknown 타입을 이용하는게 훨씬 안전한 선택이다.
아무런 값도 없음을 의미하는 타입
보통은 아무런 값도 반환하지 않는 함수의 반환값 타입을 정의할때 사용한다.
function func2(): void {
console.log("hello");
}
변수의 타입으로도 void를 지정할 수 있지만, void 타입의 변수에는 undefined 이외의 다른 타입의 값은 담을 수 없다.
let a: void;
a = undefined;
불가능을 의미하는 타입
함수가 어떠한 값도 반환할 수 없는 상황일 때 해당 함수의 반환값 타입을 정의할 때 사용된다.
function func3(): never {
while (true) {}
}
함수 func3는 무한 루프를 돌기 때문에 아무런 값도 반환할 수 없다. 엄밀히 말하면 이 함수는 영원히 종료될 수 없기 때문에 뭔가를 반환한다는 것 자체가 '불가능' 하다.
이렇게 불가능 한 값의 타입을 정의할 때 never 타입을 사용한다.\
변수의 타입을 never로 정의하면 any를 포함해 그 어떠한 타입의 값도 이 변수에 담을 수 없게 된다.
let anyVar: any;
let a: never;
a = 1; // ❌
a = null; // ❌
a = undefined; // ❌
a = anyVar; // ❌
타입스크립트에서는 다음과 같이 배열 타입을 정의한다.
let numArr: number[] = [1, 2, 3]
let strArr: string[] = ["hello", "im", "winterlood"];
let boolArr: Array<boolean> = [true, false, true];
처럼 꺽쇠와 함께 타입을 작성하는 문법을 제네릭 이라고 한다.
number[] 와 Array 두 형식 모두 모양만 다를뿐 기능은 동일하다.
let multiArr: (number | string)[] = [1, "hello"];
소괄호바 | 를 이용해 배열 요소가 둘 중 하나의 타입에 해당하도록 타입을 정의한다.
number | string는 요소가 number이거나 string 타입이여야한다.
let doubleArr : number[][] = [
[1, 2, 3],
[4, 5],
];
다음과 같이 []를 연달아 작성해 다차원 배열 타입도 간단하게 정의 가능하다.
튜플 타입을 이용하면 고정된 길이를 갖는 배열 타입을 정의할 수 있다.
길이가 2로 고정된 number 타입 요소를 갖는 튜플 타입은 아래와 같이 정의한다.
let tup1: [number, number] = [1, 2];
다양한 타입을 갖는 튜플 타입도 정의할 수 있다.
let tup2: [number, string, boolean] = [1, "hello", true];
하지만 튜플은 결국 배열이고, 배열 메서드인 push나 pop을 이용해 고정된 길이를 무시하고 요소를 추가하거나 삭제할 수 있다.
따라서 튜플을 사용할때는 최대한 배열 메서드를 이용해 요소를 추가하거나 삭제하는 등의 연산을 할때에는 주의해야한다. 이럴때는 readonly를 함께 사용하면 수정 메서드 사용을 원천적으로 막을 수 있다.
let tup: readonly [number, string] = [1, "hello"];
tup.push(2); // ❌
객체의 기본 타입 정의는 object 이다.
let user: object = {
id: 1,
name: "챙",
};
하지만 object는 단순 값이 객체임을 표현하는 것 외에는 아무런 정보도 제공하지 않기 때문에 내부의 user.id 처럼 접근하려는 순간 타입에러가 발생한다. 이렇게 객체의 키까지 타입으로 만들고 싶으면 객체 리터럴 타입을 사용해야한다.
let user: {
id: number;
name: string;
} = {
id: 1,
name: "챙",
};
user.id;
객체 리터럴과 비슷한 문법으로 객체 타입을 정의한 타입을 객체 리터럴 타입이라고 부른다.
변수의 타입을 객체 리터럴 타입으로 정의하면 타입내에 정의되어 있는 프로퍼티(user.id)에도 접근할 수 있다.
따라서 객체의 타입을 정의할 때에는 앞으로 object 보다는 객체 리터럴 타입을 사용하는게 좋다.
여기서 알 수 있는 사실은 타입스크립트는 기존의 정적 타입 시스템을 따르는 언어인 C나 Java와는 달리 객체의 타입을 정의할 때 프로퍼티를 기준으로 객체의 구조를 정의하 듯이 타입을 정의한다는 점이다.
타입스크립트의 이런 특징을 구조적 타입 시스템이라고 부른다.
객체의 구조를 결정하는 것은 프로퍼티이고, 타입스크립트는 이 객체에 어떤 프로퍼티들이 있어야 하는지 정의하는 방식으로 객체의 타입을 정의한다.
interface Person {
name: string;
age: number;
}
interface Dog {
name: string;
age: number;
}
let man: Person = { name: "철수", age: 30 };
let myDog: Dog = { name: "바둑이", age: 3 };
// Person 자리에 Dog를 넣으려 하면?
man = myDog; // 성공! (에러 없음)
위의 코드를 보면 Person 이나 Dog와 같은 타입의 이름은 중요하지 않고 구조를 기준으로만 판단한다.
그래서 서로 다른 이름의 타입이라도, 내부 구조(프로퍼티)가 겹치면 서로 호환해서 사용할 수 있다.
선택적 프로퍼티(Optional Property)
있어도 되고 없어도 되는 상황에서 사용한다.
let user: {
id?: number; // 선택적 프로퍼티가 된 id
name: string;
} = {
id: 1,
name: "",
};
user = {
name: "김개발",
};
id 프로퍼티 뒤에 붙은 물음표의 의미는 이 프로퍼티는 이제 생략 가능한 선택적(Optional) 프로퍼티라는 의미이다.
읽기전용 프로퍼티(Readonly Property)
프로퍼티의 이름 앞에 readonly 키워드를 붙이면 된다.
let user: {
id?: number;
readonly name: string; // name은 이제 Readonly 프로퍼티가 되었음
} = {
id: 1,
name: "챙",
};
user.name = "김개발"; // ❌
name 프로퍼티는 이제 읽기 전용 프로퍼티가 되었기 때문에 마지막 라인처럼 프로퍼티의 값을 수정하려고 하면 오류가 발생하게 된다. 이를 통해 의도치 않은 프로퍼티의 수정을 방지할 수 있다.
변수를 선언하듯 타입을 별도로 정의할 수 있다.
type User = {
id: number;
name: string;
nickname: string;
birth: string;
bio: string;
location: string;
};
변수처럼 동일한 스코프에 동일한 이름의 타입 별칭을 선언하는 건 불가하다. 하지만 스코프가 다르면 가능하다.
둘 다 객체의 타입을 정의할 수 있다는 점은 비슷하다.
하지만 type은 객체뿐만 아니라 모든 타입(원시값, 유니온 등)에 별칭을 붙일 수 있는 반면, interface는 객체의 구조를 정의하는 데 특화되어 있다는 차이가 있다.
객체 타입을 유연하게 정의할 수 있도록 돕는 특수한 문법이다.
type CountryCodes = {
[key: string]: string;
};
let countryCodes: CountryCodes = {
Korea: "ko",
UnitedState: "us",
UnitedKingdom: "uk",
// (... 약 100개의 국가)
Brazil : 'bz'
};
[key : string] : string 은 인덱스 시그니쳐 문법으로 이 객체 타입에는 key가 string 타입이고 value가 string 타입인 모든 프로퍼티를 포함된다 라는 의미이다.
따라서 Korea : “ko”나 Brazil : “bz” 처럼 key와 value가 모두 string 타입인 이런 프로퍼티들 이 타입에 자동으로 모두 포함되고, 굳이 일일이 이런 타입들을 직접 명시할 필요가 없다.
type CountryNumberCodes = {
[key: string]: number;
Korea: string; // ❌
};
하지만 주의할 점은 인덱스 시그니처를 사용하면서 동시에 추가적인 프로퍼티를 정의할때에는 인덱스 시그니처의 value와 추가한 value의 타입이 호환되거나 일치해야한다.
여러개의 값을 나열하는 용도로 사용
// enum 타입
// 여러가지 값들에 각각 이름을 부여해 열거해두고 사용하는 타입
enum Role {
ADMIN = 0, // 0 할당(자동)
USER = 1, // 1 할당(자동)
GUEST = 2, // 2 할당(자동)
}
const user1 = {
name: "이정환",
role: Role.ADMIN, //관리자
};
const user2 = {
name: "홍길동",
role: Role.USER, // 회원
};
const user3 = {
name: "아무개",
role: Role.GUEST, // 게스트
};
user1.role 에는 0이 user2.role에는 1이 user3.role에는 2가 할당된다.
이렇듯 유저의 권한과 같은 여러개의 멤버를 갖는 값을 숫자로 표기할 때 enum을 이용해 보다 안전하고 직관적으로 관리할 수 있습니다.
참고로 다음과 같이 enum 멤버에 숫자 값을 직접 할당하지 않아도 0 부터 1씩 늘어나는 값으로 자동으로 할당된다. 추가로 문자열도 할당 가능하다.
enum Language {
korean = "ko",
english = "en",
}
Enum은 컴파일 결과 객체가 된다.
enum은 컴파일될 때 다른 타입들 처럼 사라지지 않고 자바스크립트 객체로 변환된다. 따라서 우리가 위에서 했던 것 처럼 값으로 사용할 수 있다.
var Role;
(function (Role) {
Role[Role["ADMIN"] = 0] = "ADMIN";
Role[Role["USER"] = 1] = "USER";
Role[Role["GUEST"] = 2] = "GUEST";
})(Role || (Role = {}));
var Language;
(function (Language) {
Language["korean"] = "ko";
Language["english"] = "en";
Language["japanese"] = "jp";
})(Language || (Language = {}));
const user1 = {}