Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team
Lead Software Engineer @ProtoPie
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
git clone https://github.com/xid-mark/lgcns.git
cd lgcns
nvm use
npm ci
npx tsc --version
npm run build:watch
/*
아래 변수는 "Hello" 라는 문자열, 2020 이라는 숫자, true 라는 boolean 값을 가질 수 있습니다.
이 3 가지를 만족하는 가장 제한된 타입 A 를 만들어서 변수에 설정하세요.
*/
let a = "Hello";
a = 2020;
a = true;
type A = "Hello" | 2020 | true;
let a: A = "Hello";
a = 2020;
a = true;
/*
아래 변수 b 를 모두 만족하는 타입 B 를 만들어서 변수에 설정하세요.
b 는 name 을 필수로 가집니다. name 은 문자열입니다.
b 는 name 이외의 모든 프로퍼티를 사용 가능합니다. 해당 프로퍼티는 숫자거나 문자열 입니다.
*/
let b = {
name: "Mark",
};
b = {
name: "Mark",
age: 38,
};
b = {
name: "Mark",
country: "Korea",
};
interface StringArray {
[index: number]: string;
}
const sa: StringArray = {}; // 옵셔널하다
sa[100] = '백';
interface StringDictionary {
[index: string]: string;
}
const sd: StringDictionary = {}; // 옵셔널하다
sd.hundred = '백';
interface StringArrayDictionary {
[index: number]: string;
[index: string]: string;
}
const sad: StringArrayDictionary = {};
// 당연히 옵셔널하다.
sad[100] = '백';
sad.hundred = '백';
interface StringDictionary {
[index: string]: string;
name: string;
}
const sd: StringDictionary = {
name: '이름' // 필수
};
sd.any = 'any'; // 어떤 프로퍼티도 가능
////////////////////////////////////////////////
interface StringDictionaryNo {
[index: string]: string;
// name: number; // (X) 인덱서블 타입이 string 값을 가지기 때문에 number 를 필수로 끌어오면 에러
}
interface B {
name: string;
[index: string]: number | string;
}
let b: B = {
name: "Mark",
};
b = {
name: "Mark",
age: 38,
};
b = {
name: "Mark",
country: "Korea",
};
/*
아래 함수 c 는 2가지 형태로 사용이 가능합니다.
첫번째는 문자열을 받아 숫자를 반환하는 것이고,
두번째는 숫자와 숫자 배열을 받아 숫자를 반환하는 것입니다.
c1 과 c2 로 구현했습니다.
c 라는 함수가 두가지 함수를 사용 가능하도록 구현하세요. (힌트 : 오버로딩)
*/
function c1(str: string): number {
return str.length;
}
function c2(num: number, numArr: number[]): number {
return num + numArr.reduce((acc, cur) => acc + cur, 0);
}
function shuffle(value: string | any[]): string | any[] {
if (typeof value === 'string')
return value
.split('')
.sort(() => Math.random() - 0.5)
.join('');
return value.sort(() => Math.random() - 0.5);
}
console.log(shuffle('Hello, Mark!')); // string | any[]
console.log(shuffle(['Hello', 'Mark', 'long', 'time', 'no', 'see'])); // string | any[]
console.log(shuffle([1, 2, 3, 4, 5])); // string | any[]
function shuffle2<T extends string | any[]>(
value: T,
): T extends string ? string : T;
function shuffle2(value: any) {
if (typeof value === 'string')
return value
.split('')
.sort(() => Math.random() - 0.5)
.join('');
return value.sort(() => Math.random() - 0.5);
}
// function shuffle2<"Hello, Mark!">(value: "Hello, Mark!"): string
shuffle2('Hello, Mark!');
// function shuffle2<string[]>(value: string[]): string[]
shuffle2(['Hello', 'Mark', 'long', 'time', 'no', 'see']);
// function shuffle2<number[]>(value: number[]): number[]
shuffle2([1, 2, 3, 4, 5]);
// error! Argument of type 'number' is not assignable to parameter of type 'string | any[]'.
shuffle2(1);
function shuffle3(value: string): string;
function shuffle3<T>(value: T[]): T[];
function shuffle3(value: string | any[]): string | any[] {
if (typeof value === 'string')
return value
.split('')
.sort(() => Math.random() - 0.5)
.join('');
return value.sort(() => Math.random() - 0.5);
}
shuffle3('Hello, Mark!');
shuffle3(['Hello', 'Mark', 'long', 'time', 'no', 'see']);
shuffle3([1, 2, 3, 4, 5]);
function c1(str: string): number {
return str.length;
}
function c2(num: number, numArr: number[]): number {
return num + numArr.reduce((acc, cur) => acc + cur, 0);
}
function c(str: string): number;
function c(num: number, numArr: number[]): number;
function c(strOrNum: string | number, numArr?: number[]): number {...}
function c(strOrNum: string | number, numArr?: number[]): number {
if (typeof strOrNum === "string") {
return c1(strOrNum);
} else {
return c2(strOrNum, numArr!);
}
}
function c(str: string): number;
function c(num: number, numArr: number[]): number;
function c(strOrNum: string | number, numArr?: number[]): number {
if (typeof strOrNum === "string") {
return strOrNum.length;
} else {
return strOrNum + numArr!.reduce((acc, cur) => acc + cur, 0);
}
}
/*
아래 조건을 만족하는 ICar 인터페이스와 Volvo 클래스를 작성하세요.
- 해당 조건을 제외하고는 사용할 수 없도록 컴파일 에러를 발생시키는 것에 주의해야 합니다.
- 아래 예시 중 주석 부분은 로그로 출력되는 내용입니다. 일치하도록 로직을 작성하세요.
- 아래 예시 중 [컴파일 에러!!] 가 나는 경우는 해당 동작을 할 수 없도록 해야합니다.
- 필요한 경우, 추가로 타입을 만들어도 좋습니다.
*/
interface ICar {}
class Volvo implements ICar {}
const myCar: ICar = new Volvo("suv");
console.log(myCar.type, myCar.isStarted); // 'suv' false
myCar.open("lf"); // '좌측 앞쪽 문이 열린다.'
console.log(myCar.doorOpenState); // { lf: true, rf: false, lb: false, rb: false }
myCar.doorOpenState.lf = false; // [컴파일 에러!!]
myCar.closeAllDoors();
console.log(myCar.doorOpenState); // { lf: false, rf: false, lb: false, rb: false }
myCar.start(); // '차가 출발한다.'
console.log(myCar.isStarted); // true
interface ICar {
type: string;
readonly isStarted: boolean;
start(): void;
open(where: "lf" | "rf" | "lb" | "rb"): void;
readonly doorOpenState: DoorsOpenState;
closeAllDoors(): void;
}
interface DoorsOpenState {
readonly lf: boolean;
readonly rf: boolean;
readonly lb: boolean;
readonly rb: boolean;
}
interface ICar {
type: string;
readonly isStarted: boolean;
start(): void;
open(where: keyof DoorsOpenState): void;
readonly doorOpenState: DoorsOpenState;
closeAllDoors(): void;
}
class Volvo implements ICar {
private _isStarted = false;
private _doorsOpenState = {
lf: false,
rf: false,
lb: false,
rb: false,
};
constructor(public type: string) {}
public start(): void {
this._isStarted = true;
console.log("차가 출발한다.");
}
public open(where: keyof DoorsOpenState): void {
console.log("좌측 앞쪽 문이 열린다.");
}
get doorOpenState() {
return this._doorsOpenState;
}
get isStarted() {
return this._isStarted;
}
public closeAllDoors(): void {
this._doorsOpenState = {
lf: false,
rf: false,
lb: false,
rb: false,
};
}
}
/*
Toast 라는 타입은 모두 type 이라는 프로퍼티를 가지고 type 은 다음과 같은 것으로 분류할 수 있다.
"AFTER_SAVED" | "AFTER_PUBLISHED" | "AFTER_RESTORE"
*/
enum ToastType {
AFTER_SAVED,
AFTER_PUBLISHED,
AFTER_RESTORE,
}
interface Toast {
type: ToastType;
createdAt: string;
}
const toasts: Toast[] = [];
/*
toasts 안에 여러개의 toast 가 있습니다.
toasts 를 인자로 받아서 각각의 toast 를 문자열로 변환하는 함수를 작성합니다.
각각의 toast 를 문자열로 바꿔 문자열 배열을 리턴하는 것으로 가정하고, 아래의 로직을 작성하세요.
- toasts 를 돌며 toast 를 꺼내서 toast 의 타입이 AFTER_SAVED 인 경우,
"저장 된 직후에 뜬다!!" 라고 문자열을 만들어냅니다.
- toasts 를 돌며 toast 를 꺼내서 toast 의 타입이 AFTER_PUBLISHED 인 경우,
"퍼블리시가 된 직후에 뜬다!!" 라고 문자열을 만들어냅니다.
- toasts 를 돌며 toast 를 꺼내서 toast 의 타입이 AFTER_RESTORE 인 경우,
"복원 된 직후에 뜬다!!" 라고 문자열을 만들어냅니다.
해당 함수를 안전하게 완성해주세요 (ToastType 이 추가된 경우에도 안전하도록 작성해보세요.)
*/
function convert(toasts) {}
enum ToastType {
AFTER_SAVED,
AFTER_PUBLISHED,
AFTER_RESTORE,
}
interface Toast {
type: ToastType,
createdAt: string,
}
const toasts: Toast[] = [...];
// toastNodes1 -> (JSX.Element | undefined)[]
const toastNodes1 = toasts.map((toast) => {
if (toast.type === ToastType.AFTER_SAVED)
return (
<div key={toast.createdAt}>
<AfterSavedToast />
</div>
);
else if (toast.type === ToastType.AFTER_PUBLISHED)
return (
<div key={toast.createdAt}>
<AfterPublishedToast />
</div>
);
else if (toast.type === ToastType.AFTER_RESTORE)
return (
<div key={toast.createdAt}>
<AfterRestoredToast />
</div>
);
});
// toastNodes2 -> JSX.Element[]
const toastNodes2 = toasts.map((toast) => {
if (toast.type === ToastType.AFTER_SAVED)
return (
<div key={toast.createdAt}>
<AfterSavedToast />
</div>
);
else if (toast.type === ToastType.AFTER_PUBLISHED)
return (
<div key={toast.createdAt}>
<AfterPublishedToast />
</div>
);
else
return (
<div key={toast.createdAt}>
<AfterRestoredToast />
</div>
);
});
// toastNodes3 -> JSX.Element[]
const toastNodes3 = toasts.map((toast) => {
if (toast.type === ToastType.AFTER_SAVED)
return (
<div key={toast.createdAt}>
<AfterSavedToast />
</div>
);
else if (toast.type === ToastType.AFTER_PUBLISHED)
return (
<div key={toast.createdAt}>
<AfterPublishedToast />
</div>
);
else if (toast.type === ToastType.AFTER_RESTORE)
return (
<div key={toast.createdAt}>
<AfterRestoredToast />
</div>
);
else return neverExpected(toast.typs);
});
function neverExpected(value: never): never {
throw new Error(`Unexpected value : ${value}`);
}
// toastNodes4 -> JSX.Element[]
const toastNodes4 = toasts.map((toast) => {
if (toast.type === ToastType.AFTER_SAVED)
return (
<div key={toast.createdAt}>
<AfterSavedToast />
</div>
);
if (toast.type === ToastType.AFTER_PUBLISHED)
return (
<div key={toast.createdAt}>
<AfterPublishedToast />
</div>
);
if (toast.type === ToastType.AFTER_RESTORE)
return (
<div key={toast.createdAt}>
<AfterRestoredToast />
</div>
);
return neverExpected(toast.typs);
});
const toastNodes5 = toasts.map((toast) => {
switch (toast.type) {
case ToastType.AFTER_SAVED:
return (
<div key={toast.createdAt}>
<AfterSavedToast />
</div>
);
case ToastType.AFTER_PUBLISHED:
return (
<div key={toast.createdAt}>
<AfterPublishedToast />
</div>
);
case ToastType.AFTER_RESTORE:
return (
<div key={toast.createdAt}>
<AfterRestoredToast />
</div>
);
default:
return neverExpected(toast.type);
}
});
function convert(toasts: Toast[]) {
return toasts.map((toast) => {
switch (toast.type) {
case ToastType.AFTER_SAVED:
return "저장 된 직후에 뜬다!!";
case ToastType.AFTER_PUBLISHED:
return "퍼블리시가 된 직후에 뜬다!!";
case ToastType.AFTER_RESTORE:
return "복원 된 직후에 뜬다!!";
default:
return neverExpected(toast.type);
}
});
}
function neverExpected(value: never): never {
throw new Error(`Unexpected value : ${value}`);
}
/*
class 의 method 에 decorator 를 사용해서 해당 class 의 this 를 자동으로 바인딩할 수 있습니다.
아래의 Timer 는 new Timer().start(); 를 이용해서 실행하면, NaN 이 출력됩니다.
_loop 메서드의 위에 @autobind 를 달아 정상적으로 출력되도록 autobind 함수를 작성하세요.
*/
function autobind() {}
class Timer {
private _count = 0;
private _timer: number | null = null;
public start() {
this._timer = setInterval(this._loop, 1000);
}
public stop() {
if (this._timer !== null) {
clearInterval(this._timer);
this._count = 0;
}
}
// @autobind
private _loop() {
console.log(this._count++);
}
}
new Timer().start();
function methodDecorator(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {...}
function methodDecoratorFactory(...) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {...};
}
function methodDecorator(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('target', target);
console.log('propertyKey', propertyKey);
console.log('descriptor', descriptor);
}
class Test3 {
@methodDecorator
public print() {}
}
// target Test3 { print: [Function] }
// propertyKey print
// descriptor { value: [Function], writable: true, enumerable: true, configurable: true }
target
클래스
propertyKey
메소드 이름
descriptor
PropertyDescriptor (lib.es5.d.ts)
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
configurable
이 속성기술자는 해당 객체로부터 그 속성을 제거할 수 있는지를 기술한다.
true 라면 삭제할 수 있다. 기본값은 false.
enumerable
해당 객체의 키가 열거 가능한지를 기술한다.
true 라면 열거가능하다. 기본값은 false.
value
속성에 해당되는 값으로 오직 적합한 자바스크립트 값 (number, object, function, etc) 만 올 수 있다.
기본값은 undefined.
writable
writable이 true로 설정되면 할당연산자 assignment operator 를 통해 값을 바꿀 수 있다.
기본값은 false.
function log(show: boolean = true) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
show && console.log('start');
original(...args);
show && console.log('end');
};
};
}
class Test5 {
@log()
print1(first: string, second: number) {
console.log('print1', first, second);
}
@log(false)
print2(first: string, second: number) {
console.log('print2', first, second);
}
}
const test5 = new Test5();
test5.print1('mark', 36);
test5.print2('mark', 36);
class Timer {
private _count = 0;
private _timer: number | null = null;
public start() {
this._timer = setInterval(this._loop, 1000);
}
public stop() {
if (this._timer !== null) {
clearInterval(this._timer);
this._count = 0;
}
}
@autobind
private _loop() {
console.log(this._count++);
}
}
function autobind(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const fn = descriptor.value;
return {
get() {
const boundFn = fn.bind(this);
return boundFn;
},
};
}
/*
state 는 읽을 수만 있고, 변경을 하려면 새로 데이터를 만들어야 하는 전형적인 immutable data 입니다.
아래 상태에서는 내부 데이터가 변경이 가능합니다.
예를 들어, state1.books[0].title = "새 책 이름"; 과 같이 변경이 가능합니다.
컴파일 타임에 변경이 불가능하도록 DeepReadonly<T> 라는 타입을 만들어보세요.
(어떠한 State 에도 대응이 가능하도록 범용적인 타입을 만들어보세요.)
*/
const state = {
books: [{ title: "책 이름", author: "저자" }],
};
type Item2<T> = {
id: T;
container: T extends string ? StringContainer : NumberContainer;
};
const item2: Item2<string> = {
id: 'aaaaaa',
container: null, // Type 'null' is not assignable to type 'StringContainer'.
};
type Item3<T> = {
id: T extends string | number ? T : never;
container: T extends string
? StringContainer
: T extends number
? NumberContainer
: never;
};
const item3: Item3<boolean> = {
id: true, // Type 'boolean' is not assignable to type 'never'.
container: null, // Type 'null' is not assignable to type 'never'.
};
type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
const promises = [Promise.resolve('Mark'), Promise.resolve(38)];
type Expected = UnpackPromise<typeof promises>; // string | number
function plus1(seed: number): number {
return seed + 1;
}
type MyReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
type Id = MyReturnType<typeof plus1>;
lookupEntity(plus1(10));
function lookupEntity(id: Id) {
// query DB for entity by ID
}
// type Exclude<T, U> = T extends U ? never : T;
type Excluded = Exclude<string | number, string>; // number - diff
// type Extract<T, U> = T extends U ? T : never;
type Extracted = Extract<string | number, string>; // string - filter
// Pick<T, Exclude<keyof T, K>>; (Mapped Type)
type Picked = Pick<{name: string, age: number}, 'name'>;
// type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Omited = Omit<{name: string, age: number}, 'name'>;
// type NonNullable<T> = T extends null | undefined ? never : T;
type NonNullabled = NonNullable<string | number | null | undefined>;
/*
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
*/
/*
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
*/
type MyParameters = Parameters<(name: string, age: number) => void>; // [name: string, age: number]
interface Constructor {
new (name: string, age: number): string;
}
/*
type ConstructorParameters<
T extends new (...args: any) => any
> = T extends new (...args: infer P) => any ? P : never;
*/
type MyConstructorParameters = ConstructorParameters<Constructor>; // [name: string, age: number]
/*
type InstanceType<T extends new (...args: any) => any> = T extends new (
...args: any
) => infer R
? R
: any;
*/
type MyInstanceType = InstanceType<Constructor>; // string
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Person {
id: number;
name: string;
hello(message: string): void;
}
type T1 = FunctionPropertyNames<Person>;
type T2 = NonFunctionPropertyNames<Person>;
type T3 = FunctionProperties<Person>;
type T4 = NonFunctionProperties<Person>;
interface IPerson {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<IPerson>;
const person: ReadonlyPerson = Object.freeze<IPerson>({
name: "Mark",
age: 38,
});
person.name = "Hanna"; // error!
person.age = 27; // error!
interface IPerson {
name: string;
age: number;
}
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type Stringify<T> = {
[P in keyof T]: string;
};
type PartialNullablePerson = Partial<Nullable<Stringify<IPerson>>>;
/*
type PartialNullablePerson = {
name?: string | null | undefined;
age?: string | null | undefined;
speak?: string | null | undefined;
}
*/
let pnp: PartialNullablePerson;
pnp = { name: 'Mark', age: '38' };
pnp = { name: 'Mark' };
pnp = { name: undefined, age: null };
// Make all properties in T optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties in T required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties in T readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// From T, pick a set of properties whose keys are in the union K
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Construct a type with a set of properties K of type T
type Record<K extends keyof any, T> = {
[P in K]: T;
};
interface Book {
title: string;
author: string;
}
interface IRootState {
book: {
books: Book[];
loading: boolean;
error: Error | null;
};
}
type IReadonlyRootState = Readonly<IRootState>;
let state1: IReadonlyRootState = {} as IReadonlyRootState;
const book1 = state1.book.books[0];
book1.title = 'new';
type DeepReadonly<T> = T extends (infer E)[]
? ReadonlyArray<DeepReadonlyObject<E>>
: T extends object
? DeepReadonlyObject<T>
: T;
type DeepReadonlyObject<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };
const state: DeepReadonly<{ books: { title: string; author: string }[] }> = {
books: [{ title: "책 이름", author: "저자" }],
};
state.books[0].author = "마크";
By Woongjae Lee
LG CNS 타입스크립트 특강 4주차
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team