TypeScript 시작하기
Why TypeScript?
function fn(x) {
return x.flip();
}
자바스크립트는 런타임에 타입을 확인하고 무엇을 할지 결정한다.
위 코드에서 fn은 특정한 타입의 x를 받아야 하고, x가 반드시 callable한 flip를 가지고 있어야 한다.
이로 인해 시스템의 동작을 예측하기 어려워지고, 프로그래머의 실수를 런타임에야 발견하게 된다.
tsc
- TypeScript compiler
- 설치하기:
npm install -g typescript
tsc hello.ts
- 타입을 체크한다.
hello.js
라는 새로운 파일이 생성된다.- 에러가 있더라도
hello.js
파일이 생성된다. (당신이 TypeScript보다 잘 알 것이기에) - 에러가 있을 때 컴파일을 하고 싶지 않다면
--noEmitOnError
옵션을 준다. - 타입스크립트 그대로 실행할 수 있는 브라우저/런타임은 없기에 먼저 컴파일을 해줘야 하는 것이다.
hello.js
는 낡은 버전의 자바스크립트로 변환된다. (default는 ES3)--target
옵션을 주어 변환 버전을 지정할 수 있다.- 요즘엔 ES2015가 대세이기에 특별히 오래된 브라우저를 지원하고 싶지 않은 한
--target ES2015
를 명시해준다.
Explit Types
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", new Date());
그러나 타입을 반드시 명시할 필요는 없다.
Strictness
타입스크립트가 타입을 얼마나 엄격하게 검사할 것인지 지정할 수 있다.
noImplicitAny
: 몇몇 경우에서 타입스크립트는 타입을 추론하지 않고 any로 간주한다.
이를 막기 위해 any로 간주되는 경우 오류를 발생시킨다.strictNullChecks
:null
과undefinded
를 보다 명시적으로 처리시킨다.
Basic Types
Primitive Types
- string
- number
- boolean
Array
string[] (= Array)
Any
- 특정 값으로 인해 타입 검사 오류를 발생시키고 싶지 않을 때 사용
- any를 사용하면 추가적인 타입 검사가 비활성화 된다.
Object
function printName(obj: { first: string; last: string }) {
// …
}
Optional
function printName(obj: { first: string; last?: string }) {
// …
}
Union
function printName(name: string | number) {
// …
}
- ‘무엇이든 하나’
- 모든 멤버에 유효한 작업만 허용 (위 코드에서 string 타입에만 유효한 메서드는 호출할 수 없다)
Type Aliases
type Point = {
x: number;
y: number;
};
function printCoord(pt: Point) {
console.log(pt.x);
console.log(pt.y);
}
printCoord({ x: 100, y: 100 });
똑같은 타입을 재사용하거나 다른 이름으로 쓰고 싶은 경우
Interfaces
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log(pt.x);
console.log(pt.y);
}
printCoord({ x: 100, y: 100 });
타입 별칭과 인터페이스의 차이?
- 인터페이스는 확장 가능하다.
- 타입 별칭은 intersection을 통해 확장한다.
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear = getBear()
bear.name
bear.honey
interface Window {
title: string
}
interface Window {
ts: TypeScriptAPI
}
const src = ‘const a = "Hello world"’;
window.ts.transpileModule(src, {});
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
const bear = getBear()
bear.name
bear.honey
우선 인터페이스를 사용하고 이후 문제가 생기면 타입을 사용한다.
Type Assertions
const myCanvas = document.getElementById("main canvas") as HTMLCanvasElement;
# 또는
const myCanvas = document.getElementById("main canvas");
- 당신이 타입스크립트보다 타입에 대해 더 잘 아는 경우
- 타입을 좀 더 구체적으로 명시할 수 있다.
- 물론 불가능한 강제 변환은 안 된다. (const x = "hello" as number;)
Literal
let x: "hello" = "hello";
// ok
x = "hello";
// err!
x = "hello2";
값의 범위를 제한할 때 사용하면 유용하다.
function printText(s: string, alignment: "left" | "right" | "center") { … }
리터럴 추론시 문제
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method); // err!
function handleRequest(url: string, method: "GET" | "POST") { … }
req.method의 타입이 "GET"이 아닌 string으로 추론되어 에러가 발생한다.
req 생성 시점과 handleRequest 호출 시점 사이에 코드 평가가 발생할 수도 있고, 이때 req.method에 새로운 문자열이 대입될 수도 있기 때문이다.
이 문제는 아래와 같은 방법으로 해결한다.
const req = { url: "https://example.com", method: "GET" as "GET" };
handleRequest(req.url, req.method as "GET");
# 또는
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
as const
는 해당 객체의 모든 프로퍼티에 리터럴 타입의 값이 대입되도록 보장한다.
null, undefined
항상 strictNullChecks
옵션을 설정하여 null, undefined를 체크하도록 한다.
strictNullChecks
가 설정되어 있으면 해당 값을 사용하기 전에 테스트를 해야한다. (if (x === undefined) { … })- null, undefined가 아니라고 단언하려면 표현식 뒤에
!
를 붙인다.
Enums
Less common primitives
- bitint
- symbol: globally unique reference
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// the condition is always ‘false’
}
Type guard
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return (padding + 1).join(" ") + input;
}
return padding + input;
}
- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
False
0
NaN
""
0n
(the bigint version of zero)null
undefined
Functions
function greeter(fn: (name: string) => void) {
fn(`Hello, ${name}`);
}
greeter(console.log("Ariana"));
Call Signatures
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned" + fn(6));
}
와 ㅇ0ㅇ
Construct Signatures
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
call, construnct signates를 합치면
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
Generic Functions
function firstElement(arr: any[]) {
return arr[0];
}
위 function은 any 타입을 리턴하기에, 이를 개선하면
function firstElement<Type>(arr: Type[]): Type {
return arr[0];
}
타입 값에 제한을 두고 싶으면
function longest<Type extends { length: number }>(a: Type, b: Type) {
return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]);
longest("a", "aaa");
longest(10, 100); //err!
- 가능한 타입 값에 제한을 두지 않고 쓰도록 한다.
- 혹시 불필요한 제네릭이 아닌지 확인한다.
Overloads
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y:number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
makeDate(12345678);
makeDate(5, 5, 5);
makeDate(1, 3); //err!
- 바디를 구현한 function을 직접 호출할 수는 없다.
The signature of the implementation is not visible from the outside. - 가능한 오버로딩보다 union type을 사용하도록 한다. (타입에 유연성을 주기 위해)
Declaring this
in a Function
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
const admins = db.filterUsers(function (this: User)) {
return this.admin;
}
- function에서
this
가 무엇을 가르키는지 명시적으로 표시할 수 있다. - arrow function에서는 이런 방식을 사용할 수 없다.
Other types working with function types
- void
- object
- unknown: represents any value but is safer because it can't do anything
- never: represents that the function throws an exception or terminates execution of the program
- Function: describes properties like bind, call, apply
Object
readonly properties
interface SomeType {
readonly prop: string;
}
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Ariana",
age: 16,
};
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); //16
writeablePerson.age++;
console.log(readonlyPerson.age); //17
Index Signatures
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; //secondItem's type is string
interface NumberDictionary {
[index: string]: number; //인덱스는 string, 그 값은 number 타입이어야 한다.
length: number;
name: string; //err! 값이 number 타입이어야 한다.
}
Extending Types
interface Address {
name: string;
street: string;
city: string;
}
interface AddressWithUnit extends Address {
unit: string;
}
- 인터페이스를 여러개 상속할 수 있다.
Intersection Types
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
function draw(circle: Colorful & Circle) {
console.log(`${circle.color} ${circle.radius}`);
}
Generic Object Types
type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
Pair
function doSomething(pair: [string, number]) {
const a = pair[0]; //string
const b = pair[1]; //number
}
Type Manipulation
keyof
object의 key들의 lieteral 값들을 가져온다.
뭔지 잘 감이 안 온다.
key를 사용하고 싶을때 key만을 enum처럼 뽑아 type으로 사용하는 것이라 이해했다. ???맞나?
Indexed Access Types
type Person = { age: number; name: string; alive: boolean };
type t1 = Person["age"]; //number
type t2 = Person["age" | "name"]; //string | number
Template Literal Types
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; //"welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
Class
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
class Point {
x = 0; //초기화
y = 0;
}
const pt = new Point();
console.log(`${pt.x}, pt.y`); //0, 0
--strictPropertyInitialization
옵션을 지정하면 클래스 필드들은 반드시 생성자에서 초기화되어야 한다.
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
생성자가 아닌 다른 수단을 통해 필드를 확실히 초기화하려는 경우(예를 들어, 외부 라이브러리가 클래스의 일부를 채우고 있을 수 있음) !
연산자를 사용한다.
class OKGreeter {
// Not initialized, but no error
name!: string;
}
Constructors
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
생성자에서 super()는 this
에 접근하기 전에 호출되어야 한다.
Class Heritage
- implements interface
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10; //err! Property 'y' does not exist on type 'C'.
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
- extends class
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog(); // Base class method
d.move(); // Derived class method d.woof(3);
Member Visibility
- public: default
- protected: subclasses에서만 접근 가능
- private
+주의! protected, private은 타입 체킹 시에만 강제된다. 런타임 시에는 접근 가능하다.
class MySafe {
private secretKey = 12345;
}
const s = mySafe();
console.log(s["secretKey"]; //ok
console.log(s.secretKey); //err! Property 'secretKey' is private and only accessible within class 'MySafe'.
Static Members
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
this
at Runtime in Classes
참고: https://poiemaweb.com/es6-arrow-function
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// Prints "obj", not "MyClass"
console.log(obj.getName());
자바스크립트에서 함수 안에서의 this의 값은 함수가 어떻게 호출되느냐에 따라 다르다. this에 바인딩될 객체가 동적으로 결정된다.
위 코드에서 함수가 obj 레퍼런스를 통해 호출되었기 때문에 this는 c가 아니라 obj이다.
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// Prints "MyClass", not "obj"
console.log(obj.getName());
화살표 함수를 사용하면 this에 바인딩될 객체가 정적으로 결정된다. 화살표 함수의 this는 언제나 상위 스코프의 this를 가리킨다. (Lexical this)