Skip to content
On this page

2020-11-11

Title
2020-11-11
Category
2020
Tags
Aliases
2020-11-11
Created
3 years ago
Updated
last year

TypeScript

목표

  • TypeScript로 타이핑을 잘하면, 런타임 전에 미리 오류를 알 수 있습니다.

  • 코드의 구현자가 사용자에게 의도를 전달할 수 있다.

타입 시스템

  • 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템

  • 컴파일러가 자동으로 타입을 추론하는 시스템

☝ 타입스크립트는 타입을 명시적으로 지정할 수 있고 명시적으로 지정하지 않으면컴파일러가 자동으로 타입을 추론한다.

명시적 타입 vs 추론

typescript
// 함수의 리턴 타입은 number로 추론된다.
// 하지만 매개변수의 타입을 명시적으로 지정하지 않아서, any로 추론된다.
function f(a) {
	return a * 38;
}

// 사용자는 a가 any 이기 때문에 사용법에 맞게 함수를 실행했지만, 사용자는 예측하지 못한 결과를 얻게 된다.
console.log(f(10)); // 380
console.log(f('Mark')); // NaN
// 함수의 리턴 타입은 number로 추론된다.
// 하지만 매개변수의 타입을 명시적으로 지정하지 않아서, any로 추론된다.
function f(a) {
	return a * 38;
}

// 사용자는 a가 any 이기 때문에 사용법에 맞게 함수를 실행했지만, 사용자는 예측하지 못한 결과를 얻게 된다.
console.log(f(10)); // 380
console.log(f('Mark')); // NaN

이것은 noImplicitAny 옵션을 통해 방어할 수 있다. 타입을 명시적으로 지정하지 않은 경우 any 로 추론되는 변수가 있으면 컴파일 에러가 발생한다.

typescript
// 매개변수의 타입을 명시적으로 지정했다.
// 명시적으로 지정하지 않은 리턴 타입은 number로 추론된다.
function f(a: number) {
	if (a > 0) {
		return a * 38;
	}
}

// 사용자는 사용법에 맞게 숫자형을 사용해서 함수를 실행했지만,
// 실제로는 함수의 리턴이 undefined로, undefined + 5가 실행되어 NaN이 출력된다.

console.log(f(5)); // 190;
console.log(f(-5) + 5); // NaN
// 매개변수의 타입을 명시적으로 지정했다.
// 명시적으로 지정하지 않은 리턴 타입은 number로 추론된다.
function f(a: number) {
	if (a > 0) {
		return a * 38;
	}
}

// 사용자는 사용법에 맞게 숫자형을 사용해서 함수를 실행했지만,
// 실제로는 함수의 리턴이 undefined로, undefined + 5가 실행되어 NaN이 출력된다.

console.log(f(5)); // 190;
console.log(f(-5) + 5); // NaN

strictNullChecks 옵션을 켜면 모든 타입에 자동으로 포함되어 있는 nullundefined 를 제거해준다.

typescript
// 이 함수의 리턴 타입은 number | undefined로 추론된다.
function f(a: number) {
	if (a > 0) {
		return a * 38;
	}
}

// 해당 함수의 리턴 타입은 number | undefined 이기 때문에,
// 타입에 따르면 컴파일 에러가 발생한다.
// 컴파일 에러를 고치기 위해 사용자와 작성자가 의논해야 한다.
console.log(f(-5) + 5); // error TS2532: Object is possibly 'undefined'
// 이 함수의 리턴 타입은 number | undefined로 추론된다.
function f(a: number) {
	if (a > 0) {
		return a * 38;
	}
}

// 해당 함수의 리턴 타입은 number | undefined 이기 때문에,
// 타입에 따르면 컴파일 에러가 발생한다.
// 컴파일 에러를 고치기 위해 사용자와 작성자가 의논해야 한다.
console.log(f(-5) + 5); // error TS2532: Object is possibly 'undefined'

하지만 가급적이면 명시적으로 리턴 타입을 지정하는 것이 좋다.

noImplicitReturns 옵션을 켜면 함수 내의 모든 코드의 줄기가 값을 리턴하지 않으면 컴파일 에러를 발생시킨다.

typescript
// error TS7030: Not all code paths return a value
function f(a: number) {
	if (a > 0) {
		return a * 38;
	}
}
// error TS7030: Not all code paths return a value
function f(a: number) {
	if (a > 0) {
		return a * 38;
	}
}

이로 인해 리턴 타입을 명시하고, 모든 줄기에 return 을 직접 하도록 구현자에게강제할 수 있다.

매개변수에 object가 들어오는 경우

javascript
function f(a) {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}

console.log(f({ name: 'Mark', age: 38 })); // 이름은 Mark이고, 연령대는 30대 입니다.
console.log(f('Mark')); // 이름은 undefined이고, 연령대는 NaN대 입니다.
function f(a) {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}

console.log(f({ name: 'Mark', age: 38 })); // 이름은 Mark이고, 연령대는 30대 입니다.
console.log(f('Mark')); // 이름은 undefined이고, 연령대는 NaN대 입니다.

JavaScript에서는 제약사항으로 알려주지 않기 때문에 런타임에서 오류를 파악할 수있다.

  • Object literal type
typescript
function f(a: { name: string, age: number }): string {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
function f(a: { name: string, age: number }): string {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
  • 나만의 타입을 만드는 방법
typescript
interface PersonInterface {
  name: string;
  age: number;
}

type PersonTypeAlias = {
  name: string;
  age: number;
}

function f1(a: PersonInterface): string {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}

function f2(a: PersonTypeAlias): string {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
interface PersonInterface {
  name: string;
  age: number;
}

type PersonTypeAlias = {
  name: string;
  age: number;
}

function f1(a: PersonInterface): string {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}

function f2(a: PersonTypeAlias): string {
  return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}

interface vs type alias

structural vs nominal type system

타입스크립트는 structural 타입 시스템을 사용한다.

  • structural type system : 구조가 같으면, 같은 타입이다.

  • nominal type system : 구조가 같아도 이름이 다르면, 다른 타입이다.

typescript
interface IPerson {
	name: string;
	age: number;
	speak(): string;
}

type PersonType = {
	name: string;
	age: number;
	speak(): string;
};
interface IPerson {
	name: string;
	age: number;
	speak(): string;
}

type PersonType = {
	name: string;
	age: number;
	speak(): string;
};
  • typescript를 nominal 처럼 쓰는 꼼수 ? - 아직 잘 이해를 못함
    typescript
    type PersonID = string & {readonly brand: unique symbol};
    
    function PersonID(id: string): PersonID {
    	return id as PersonID;
    }
    
    function getPersonById(id: PersonID) {}
    
    getPersonById(PersonID('id-aaaaaa'));
    getPersonById('id-aaaaaa');
    
    type PersonID = string & {readonly brand: unique symbol};
    
    function PersonID(id: string): PersonID {
    	return id as PersonID;
    }
    
    function getPersonById(id: PersonID) {}
    
    getPersonById(PersonID('id-aaaaaa'));
    getPersonById('id-aaaaaa');
    
  • function
    typescript
    type EatType = (food: string) => void;
    
    interface IEat {
    	(food: string): void;
    }
    
    type EatType = (food: string) => void;
    
    interface IEat {
    	(food: string): void;
    }
    
  • array
    typescript
    type PersonList = string[];
    
    interface IPersonList {
    	[index: number]: string;
    }
    
    type PersonList = string[];
    
    interface IPersonList {
    	[index: number]: string;
    }
    
  • intersection
    typescript
    interface ErrorHandling {
    	success: boolean;
    	error?: {message: string};
    }
    
    interface ArtistsData {
    	artists: {name: string}[];
    }
    
    type ArtistsResponseType = ArtistsData & ErrorHandling;
    
    interface IArtistsResponse extends ArtistsData, ErrorHandling {}
    
    interface ErrorHandling {
    	success: boolean;
    	error?: {message: string};
    }
    
    interface ArtistsData {
    	artists: {name: string}[];
    }
    
    type ArtistsResponseType = ArtistsData & ErrorHandling;
    
    interface IArtistsResponse extends ArtistsData, ErrorHandling {}
    
  • union
    typescript
    interface Bird {
    	fly(): void;
    	layEggs(): void;
    }
    
    interface Fish {
    	swim(): void;
    	layEggs(): void;
    }
    
    type BirdType = {
    	fly(): void;
    	layEggs(): void;
    };
    
    type FishType = {
    	swim(): void;
    	layEggs(): void;
    };
    
    // union은 type alias를 사용해야 한다.
    type PetType = Bird | Fish;
    
    // 에러 발생
    interface IPet extends PetType {}
    class Pet implements PetType {}
    
    // 가능
    interface IBird extends BirdType {}
    class IFish implements FishType {}
    
    interface Bird {
    	fly(): void;
    	layEggs(): void;
    }
    
    interface Fish {
    	swim(): void;
    	layEggs(): void;
    }
    
    type BirdType = {
    	fly(): void;
    	layEggs(): void;
    };
    
    type FishType = {
    	swim(): void;
    	layEggs(): void;
    };
    
    // union은 type alias를 사용해야 한다.
    type PetType = Bird | Fish;
    
    // 에러 발생
    interface IPet extends PetType {}
    class Pet implements PetType {}
    
    // 가능
    interface IBird extends BirdType {}
    class IFish implements FishType {}
    

Declaration Merging - interface

  • 중복되는 이름으로 선언되었을 때 머지되는 기능은 interface에서만 제공된다.

  • type은 중복시에 에러가 발생한다.

  • 이는 외부 유틸에서 사용자가 원하는 타입을 추가해서 사용하는 것이 가능하다.

typescript
interface MergingInterface {
	a: string;
}

interface MergingInterface {
	b: string;
}

let mi: MergingInterface;
// mi: { a: string, b: string }
interface MergingInterface {
	a: string;
}

interface MergingInterface {
	b: string;
}

let mi: MergingInterface;
// mi: { a: string, b: string }

언제 type을 사용하고 언제 interface를 사용하는가

  • 의미적으로, 역할적으로 type alias를 타입에 별칭을 붙일 때 사용한다.

  • 즉, 이미 있는 타입에 별칭을 붙여 다른 이름으로 부르고 싶을 때, 이미 있는 타입을 union으로 조합할 때 사용한다.

  • 그 외에 새로운 타입을 생성할 때 interface를 사용한다.

    • 실제 동작 상의 차이는 Merging에서의 차이가 가장 중요하다.

서브 타입과 슈퍼 타입

집합의 관계에서 포함되는 쪽이 서브 타입, 포함하는 쪽이 슈퍼 타입이다.

typescript
let sub1: 1 = 1;
let sup1: number = sub1;
sub1 = sup1; // error! Type 'number' is not assignable to type '1'.

let sub2: number[] = [1];
let sup2: object = sub2;
sub2 = sup2; // error! Type '{}' is missing the following the properties from type 'number[]': length, pop, push, concat, and 16 more.

let sub3: [number, number] = [1, 2];
let sup3: number[] = sub3;
sub3 = sup3; // error! Type 'number[]' is not assignable to type '[number, number]'. Target requires 2 element(s) but source may have fewer.

let sub4: number = 1;
let sup4: any = sub4;
sub4 = sup4;

let sub5: never = 0 as never;
let sup5: number = sub5;
sub5 = sup5; // Type 'number' is not assignable to type 'never'.

class Animal {}
class Dog extends Animal {
	eat() {}
}

let sub6: Dog = new Dog();
let sup6: Animal = sub6;
sub6 = sup6; // Property 'eat' is missing in type 'Animal' but required in type 'Dog'.
let sub1: 1 = 1;
let sup1: number = sub1;
sub1 = sup1; // error! Type 'number' is not assignable to type '1'.

let sub2: number[] = [1];
let sup2: object = sub2;
sub2 = sup2; // error! Type '{}' is missing the following the properties from type 'number[]': length, pop, push, concat, and 16 more.

let sub3: [number, number] = [1, 2];
let sup3: number[] = sub3;
sub3 = sup3; // error! Type 'number[]' is not assignable to type '[number, number]'. Target requires 2 element(s) but source may have fewer.

let sub4: number = 1;
let sup4: any = sub4;
sub4 = sup4;

let sub5: never = 0 as never;
let sup5: number = sub5;
sub5 = sup5; // Type 'number' is not assignable to type 'never'.

class Animal {}
class Dog extends Animal {
	eat() {}
}

let sub6: Dog = new Dog();
let sup6: Animal = sub6;
sub6 = sup6; // Property 'eat' is missing in type 'Animal' but required in type 'Dog'.

anynever 는 추후에 다시 학습

같거나 서브 타입인 경우 할당이 가능하다. ⇒ 공변

typescript
let sub7: string = '';
let sup7: string | number = sub7;

// object - 각각의 프로퍼티가 대응하는 프로퍼티와 같거나 서브타입이어야 한다.
let sub8: {a: string; b: number} = {a: '', b: 1};
let sup8: {a: string | number; b: number} = sub8;

// array - object와 마찬가지
let sub9: Array<{a: string; b: number}> = [{a: '', b: 1}];
let sup9: Array<{a: string | number; b: number}> = sub9;
let sub7: string = '';
let sup7: string | number = sub7;

// object - 각각의 프로퍼티가 대응하는 프로퍼티와 같거나 서브타입이어야 한다.
let sub8: {a: string; b: number} = {a: '', b: 1};
let sup8: {a: string | number; b: number} = sub8;

// array - object와 마찬가지
let sub9: Array<{a: string; b: number}> = [{a: '', b: 1}];
let sup9: Array<{a: string | number; b: number}> = sub9;

함수의 매개변수 타입만 같거나 슈퍼타입인 경우, 할당이 가능하다 ⇒ 반병

typescript
class Person {}
class Developer extends Person {
	coding() {}
}
class StartupDeveloper extends Developer {
	burning() {}
}

function tellme(f: (d: Developer) => Developer) {}

// Developer => Developer 에다가 Developer => Developer 를 할당하는 경우
tellme(function dToD(d: Developer): Developer {
	return new Developer();
});

// Developer => Developer 에다가 Person => Developer 를 할당하는 경우
// 반병
tellme(function pToD(d: Person): Developer {
	return new Developer();
});

// Developer => Developer 에다가 StartupDeveloper => Developer 를 할당하는 경우
// strictFunctionType으로 에러 표시 가능
tellme(function sToD(d: StartupDeveloper): Developer {
	return new Developer();
});
class Person {}
class Developer extends Person {
	coding() {}
}
class StartupDeveloper extends Developer {
	burning() {}
}

function tellme(f: (d: Developer) => Developer) {}

// Developer => Developer 에다가 Developer => Developer 를 할당하는 경우
tellme(function dToD(d: Developer): Developer {
	return new Developer();
});

// Developer => Developer 에다가 Person => Developer 를 할당하는 경우
// 반병
tellme(function pToD(d: Person): Developer {
	return new Developer();
});

// Developer => Developer 에다가 StartupDeveloper => Developer 를 할당하는 경우
// strictFunctionType으로 에러 표시 가능
tellme(function sToD(d: StartupDeveloper): Developer {
	return new Developer();
});

any

  • 입력은 마음대로

  • 함수 구현이 자유롭게 ⇒ 자유가 항상 좋은 건 아니다.

unknown

  • 입력은 마음대로

  • 함수 구현은 문제 없도록

타입 추론 이해하기

  • let과 const의 타입 추론 (+ as const)

  • as const

typescript
let a = 'Mark'; // string
const b = 'Mark'; // 'Mark' => literal type

let c = 38; // number
const d = 38; // 38 => literal type

let e = false; // boolean
const f = false; // false => literal type

let g = ['Mark', 'Haeun']; // string[]
const h = ['Mark', 'Haeun']; // string[]

const i = ['Mark', 'Haeun', 'Bokdang'] as const; // readonly ['Mark', 'Haeun', 'Bokdang']
let a = 'Mark'; // string
const b = 'Mark'; // 'Mark' => literal type

let c = 38; // number
const d = 38; // 38 => literal type

let e = false; // boolean
const f = false; // false => literal type

let g = ['Mark', 'Haeun']; // string[]
const h = ['Mark', 'Haeun']; // string[]

const i = ['Mark', 'Haeun', 'Bokdang'] as const; // readonly ['Mark', 'Haeun', 'Bokdang']
  • best common type (가장 공통적인 타입을 추론해낸다)
typescript
let j = [0, 1, null]; // (number | null)[]
const k = [0, 1, null]; // (number | null)[]

class Animal {}
class Rhino extends Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}

let l = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const m = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const n = [new Animal(), new Rhino(), new Elephant(), new Snake()]; // Animal[]
const o: Animal[] = [new Rhino(), new Elephant(), new Snake()]; // Animal[]
let j = [0, 1, null]; // (number | null)[]
const k = [0, 1, null]; // (number | null)[]

class Animal {}
class Rhino extends Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}

let l = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const m = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const n = [new Animal(), new Rhino(), new Elephant(), new Snake()]; // Animal[]
const o: Animal[] = [new Rhino(), new Elephant(), new Snake()]; // Animal[]
  • Contextual Typing - 위치에 따라 추론이 다름
typescript
// Parameter 'e' implicitly has an 'any' type.
const click = (e) => {
	e; // any
};

document.addEventListener('click', click);
document.addEventListener('click', (e) => {
	e; // MouseEvent
});
// Parameter 'e' implicitly has an 'any' type.
const click = (e) => {
	e; // any
};

document.addEventListener('click', click);
document.addEventListener('click', (e) => {
	e; // MouseEvent
});

Type Guard로 안전함을 파악하기

typeof Type Guard - 보통 Primitive 타입일 경우

  • typeof로 primitive 타입을 걸러낼 수 있다.
typescript
function getNumber(value: number | string): number {
	value; // number | string
	if (typeof value == 'number') {
		return value;
	}
	value; // string
	return -1;
}
function getNumber(value: number | string): number {
	value; // number | string
	if (typeof value == 'number') {
		return value;
	}
	value; // string
	return -1;
}

instanceof Type Guard - Error 객체 구분에 많이 쓰인다.

typescript
class NegativeNumberError extends Error {}

function getNumber(value: number): number | NegativeNumberError {
	if (value < 0) return new NegativeNumberError();

	return value;
}

function main() {
	const num = getNumber(-10);

	if (num instanceof NegativeNumberError) {
		return;
	}

	num; // number
}
class NegativeNumberError extends Error {}

function getNumber(value: number): number | NegativeNumberError {
	if (value < 0) return new NegativeNumberError();

	return value;
}

function main() {
	const num = getNumber(-10);

	if (num instanceof NegativeNumberError) {
		return;
	}

	num; // number
}

in operator Type Guard - object의 프로퍼티 유무로 처리하는 경우

typescript
interface Admin {
	id: string;
	role: string;
}

interface User {
	id: string;
	emain: string;
}

function redirect(user: Admin | User) {
	if ('role' in user) {
		routeToAdminPage(user.role);
	} else {
		routeToHomePage(user.email);
	}
}
interface Admin {
	id: string;
	role: string;
}

interface User {
	id: string;
	emain: string;
}

function redirect(user: Admin | User) {
	if ('role' in user) {
		routeToAdminPage(user.role);
	} else {
		routeToHomePage(user.email);
	}
}

literal Type Guard - object의 프로퍼티가 같고, 타입이 다른 경우

typescript
interface IMachine {
	type: string;
}

class Car implements IMachine {
	type: 'CAR';
	wheel: number;
}

class Boat implements IMachine {
	type: 'BOAT';
	motor: number;
}

function getWheelOrMotor(machine: Car | Boat): number {
	if (machine.type === 'CAR') {
		return machine.wheel;
	} else {
		return machine.motor;
	}
}
interface IMachine {
	type: string;
}

class Car implements IMachine {
	type: 'CAR';
	wheel: number;
}

class Boat implements IMachine {
	type: 'BOAT';
	motor: number;
}

function getWheelOrMotor(machine: Car | Boat): number {
	if (machine.type === 'CAR') {
		return machine.wheel;
	} else {
		return machine.motor;
	}
}

custom Type Guard

typescript
function getWheelOrMoter(machine: any): number {
	if (isCar(machine)) {
		return machine.wheel;
	} else if (isBoat(machine)) {
		return machine.motor;
	} else {
		return -1;
	}
}

function isCar(arg: any): arg is Car {
	return arg.type === 'CAR';
}

function isBoat(arg: any): arg is Boat {
	return arg.type === 'BOAT';
}
function getWheelOrMoter(machine: any): number {
	if (isCar(machine)) {
		return machine.wheel;
	} else if (isBoat(machine)) {
		return machine.motor;
	} else {
		return -1;
	}
}

function isCar(arg: any): arg is Car {
	return arg.type === 'CAR';
}

function isBoat(arg: any): arg is Boat {
	return arg.type === 'BOAT';
}

Conditional Type

  • `Item T`
    typescript
    interface StringContainer {
    	value: string;
    	format(): string;
    	split(): string[];
    }
    
    interface NumberContainer {
    	value: number;
    	nearestPrime: number;
    	round(): number;
    }
    
    type Item1<T> = {
    	id: T;
    	container: any;
    };
    
    const item1: Item1<string> = {
    	id: 'aaaaaa',
    	container: null, // container type에 any를 사용했기 때문에 null을 넣는 것도 가능하다.
    };
    
    interface StringContainer {
    	value: string;
    	format(): string;
    	split(): string[];
    }
    
    interface NumberContainer {
    	value: number;
    	nearestPrime: number;
    	round(): number;
    }
    
    type Item1<T> = {
    	id: T;
    	container: any;
    };
    
    const item1: Item1<string> = {
    	id: 'aaaaaa',
    	container: null, // container type에 any를 사용했기 때문에 null을 넣는 것도 가능하다.
    };
    
  • `Item T` - T가 string이면 StringContainer, 아니면 NumberContainer
    typescript
    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 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'.
    };
    
  • `Item T` - T가 string이면 StringContainer, number면 NumberContainer, 아니면 사용 불가
    typescript
    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 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'.
    };
    
  • `ArrayFilter T`
    typescript
    type ArrayFilter<T> = T extends any[] ? T : never;
    type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;
    
    type ArrayFilter<T> = T extends any[] ? T : never;
    type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;
    

1:11:12 까지 시청 ...

Released under the MIT License.