📘
til ) TypeScript 복습하기 05
June 05, 2024
Object Types
- JS에서는 근본적으로 객체를 통해 데이터들을 모으고, 전달하며, TS는 이걸 기반으로 타입화 한다.
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
// interface 활용 방법
interface Person {
name: string;
age: number;
}
// type 활용방법
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
Quick Reference
- 치트시트를 확인하면 빠르게 사용 방법을 볼 수 있을 거다!
Property Modifiers
- 각 프로퍼티는 객체 안에서 사용되거나, 옵셔널인지 여부 등을 지정해 줄 수 있다.
Optional Properties
?
마크를 붙임으로써 해당 값의 존재 여부를 선택적으로 지정해줄 수 있다.
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
- 그리고 이러한 옵셔널한 설정은
strictNullChecks
와 함께 TS에선 undefined 가 될 잠재성이 있다고 판단하여, 에러를 보여주곤 합니다.
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
// (property) PaintOptions.xPos?: number | undefined
let yPos = opts.yPos;
// (property) PaintOptions.yPos?: number | undefined
// ...
}
- 따라서 간단하게 검사를 하는 것을 추가하여 수저을 하거나
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos === undefined ? 0 : opts.xPos;
// let xPos: number
let yPos = opts.yPos === undefined ? 0 : opts.yPos;
// let yPos: number
// ...
}
- JS에서 제공하는 방식을 통해 해결도 가능하다. 구조 분해 패턴, 기본값을 적용하여 내부 값이 옵셔널일 때 기본값을 가질 수 있어서 undefined가 나지 않도록 조치를 취한다.
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
// (parameter) xPos: number
console.log("y coordinate at", yPos);
// (parameter) yPos: number
// ...
}
readonly Properties
- readonly 라는 표시를 한 프로퍼티들은 읽을 순 있으나 재 할당은 불가능하다.
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
obj.prop = "hello";
// Cannot assign to 'prop' because it is a read-only property.
}
- 단, 해당 수정자를 쓴다고 값이 완전히 변경 불가능한 것은 아니다. 그 내부 값은 수정이 변경이 가능하거나, 해당 readonly 수정자를 붙인 객체에 한해 수정이 불가능하다고 보면 된다.
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {
// Cannot assign to 'resident' because it is a read-only property.
name: "Victor the Evictor",
age: 42,
};
}
- 읽기 전용이 의미하는 바가 읽기 전용이라는 말이 의미하는 바를 명확하게 하기 위함일 뿐이라는 것을 이해해야 하고, 상황에 따라선 TS에선 해당 유형과 호환되는 읽기 전용 속성을 덮어 씌우는 것,mapping 하는 것도 가능하다.
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
Index Signatures
- 인덱스 시그니처는 프로퍼티를 인덱스 식으로 저장하는 방식을 말한다.
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
// const secondItem: string
- 인덱스 시그니쳐의 프로퍼티로는 string, number, symbol, 템플릿 string 패턴, 유니온 타입 등을 지원한다.
- string 인덱스 시그니쳐 는 사전형 패턴을 표현하는데 매우 강력한 방법이고, 그렇기 때문에 이 인덱스 시그니처는 모든 프로퍼티들의 반환 타입을 동일하게 만드는 것을 강제하는 특성을 가지고 있다. 왜냐하면 스트링 인덱스가
obj.property
를 선언하는 것은 동시에obj["property"]
를 선언하는 것과 같기 때문이다.
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
//Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
- 그러나 프로퍼티들의 다른 타입들도 유니온 타입으로 수정하면 포괄하도록 만들 수 있다.
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
- 또한 인덱스 시그니처를 읽기 전용으로 하는 것도 가능하다.
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// Index signature in type 'ReadonlyStringArray' only permits reading.
Excess Property Checks
- 객체가 생성되거나 하는 동안 객체의 타입에 대해 철저하게 유효성을 검사할 수 있다.
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 });
// Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
- 위의 예시에서
colour
이라는 잘못된 프로퍼티가 들어가 있음에 대해 조용하게 아무런 조치 없이 끝나게 된다. - TS에서는 이러한 상황이 나타날 때 프로퍼티의 초과 분이 있는지, 필요한 프로퍼티가 정확하게 있는지 검사하고 이에 대해 경고한다.
- 단, 이러한 검사를 피하고 싶다면 타입 assertion을 통해 검사를 스킵할 수 있다.
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
- 하지만 이러한 방식보다 효과적으로 초과 프로퍼티에 대해 검사를 피하면서도, 값을 넣고 싶을 상황이 있을 수 있다. 이러한 경우 인덱스 시그니처를 활용하는 것이 효과적인 방법이 될 수 있다.
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
Extending Types
- 기본적으로 추가적인 요소가 필요하다고 할 때, 타입을 새로 지정하는 것 보다 확장하는 방식을 사용할 수 있을 것이다.
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
// 확장된 버전
interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
}
// 이렇게 확장시키는게 훨씬 용이하다
interface AddressWithUnit extends BasicAddress {
unit: string;
}
- 인터페이스와
extends
키워드를 통해 다수의 인터페이스를 확장에 사용하는 것도 가능하다.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
Intersection Types
- 인터페이스를 활용하여 다른 타입들에서 새로운 타입을 만들 수 있는데, TS 는 intersection type 이라고 불리는 새로운 타입 생성 방식을 제공한다.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
- 이러한 방식의 타입은 당연히 기존의 타입들의 멤버들을 모두 가지게 된다.
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 });
// Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
Interfaces vs. Intersections
- interface 를 활용, extends 키워드로 새로운 타입을 만드는 것과, intersection 으로 만드는 타입 방식 두 가지가 존재한다.
- 이때, 주요 차이점은 충돌을 처리하는 방법이며, 따라서 어떤 처리 방법이 선호 되냐에 따라서 두 방법 중 하나의 방법을 활용한다고 보면 된다.
Generic Object Types
- Box 라는 타입을 만든다고 하고, 어떤 any를 통해 쉽게 구현이 될 것이다.
interface Box {
contents: any;
}
- 이는 구현은 쉽지만 당연히 위험요소가 있는 타입이니 만큼, 반드시 검증이 필요한
unknown
타입을 적용하고, 타입 검증하는 방식으로 수정이 가능할 것이다.
interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
// we could check 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
// or we could use a type assertion
console.log((x.contents as string).toLowerCase());
- 이 외의 방법으로 type safe 한 접근 법으로 Box 타입에 대한 컨텐츠 별 새로운 타입으로 구분하는 방법이 있을 수 있다.
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}
- 그러나 이렇게 만드는 경우, 여전히 많은 양의 함수 오버로딩이 필요해진다. 그렇기에 제네릭을 이용하는 방법을 활용해 볼 수 있을 것이다. 이 방식을 활용하면 오버로드를 활용할 필요가 없어지므로, 그만큼 타입에 대해 자연스럽게 해결 이 가능하다.
interface Box<Type> {
contents: Type;
}
The Array Type
- 배열 타입도 존재한다. 해당 타입은
number[]
=Array<number>
와 같은 타입을 표현하는 말이라고 보면 된다.
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
- 기본적으로 Array 타입은 제네릭 타입이다.
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
// ...
}
- JS의 경우
Array
외에도Map
,set
,promise
와 같은 제네릭 타입을 제공한다.
The ReadonlyArray Type
- 배열이 확실하게 읽기 전용이라고 명시할 수 있는 특수 타입으로 이 타입이 존재 한다. 한가지 특이 사항은 이러한
ReadonlyArray
타입의 경우 생성자가 존재하지 않으며, 일반적인 배열을 할당하는 방식으로 생성이 가능하다.
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
// Property 'push' does not exist on type 'readonly string[]'.
}
// 불가능한 생성 방법
new ReadonlyArray("red", "green", "blue");
// 'ReadonlyArray' only refers to a type, but is being used as a value here.
// 이렇게 사용해야 한다.
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
- TS에서는 해당 배열의 축약형 문법을 제공해서
Array<Type>
은Type[]
으로ReadonlyArray<Type>
은readonly Type[]
으로 적으면 동일한 데이터 타입이다.
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
// Property 'push' does not exist on type 'readonly string[]'.
}
- 마지막으로
readonly
한정자가 붙은 객체에 대해서는 양방향으로 할당이 불가능하다.
let x: readonly string[] = [];
let y: string[] = [];
x = y;
y = x;
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
Tuple Types
- 튜플 타입은 배열의 일종이다. 혼합된 값을 포함하는 형태로 0 , 1 번 인덱스가 각 값을 가리키고 있다.
type StringNumberPair = [string, number];
function doSomething(pair: [string, number]) {
const a = pair[0];
const a: string
const b = pair[1];
const b: number
// ...
}
doSomething(["hello", 42]);
- 만약 범위 인덱스를 넘어서면 당연히 에러를 발생시킨다.
function doSomething(pair: [string, number]) {
// ...
const c = pair[2];
// Tuple type '[string, number]' of length '2' has no element at index '2'.
}
- 동시에 튜플을 분해하여 적용시키는 것도 가능하다. `
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
console.log(inputString);
// const inputString: string
console.log(hash);
// const hash: number
}
- 숫자 리터럴 유형으로
length
를 선언한 배열 버전과 튜플은 동일한 역할을 한다.
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}
- 옵셔널로 값을 넣는 것도 가능한데, 이때 옵셔널 튜플 요소는 끝에만 표시가 가능하다. 또한 이에 따라
length
도 달라질 수 있다.
type Either2dOr3d = [number, number, number?]; // 옵셔널은 마지막엔만
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
// const z: number | undefined
console.log(`Provided coordinates had ${coord.length} dimensions`);
// (property) length: 2 | 3
- 튜플은 또한
rest
가변 인자를 추가로 넣어줄 수 있다. 이때는 length 값이 존재하지 않는다.
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
- 이러한 옵셔널과 rest 를 활용하는 것은, TS에서 다음과 같이 사용이 가능하다는 점에서 응용의 가치가 있다. 뿐만 아니라 전체의 값의 목록 중에서 필요시 되는 부분만 인자로 사용하는 방식이 가지는 이점도 있기 때문에 인지할 필요가 있다.
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
// 위의 예시와 아래의 예시는 실질적으로 동등하다.
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}
readonly Tuple Types
readonly
한정어를 통해 튜플을 읽기 전용으로 만들수 있다. 이는 다른 경우와 마찬가지로 명시적으로 만들어주는 역할을 한다.
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
// Cannot assign to '0' because it is a read-only property.
}
- 튜플은 용도 자체가 수정되지 않고 지속적으로 값을 유지하는 것이 좋으므로
readonly
로 설정하는게 좋으며, 이를 위해const
단정을 통해 해당 값을 만들어 줄 수 있다. 단 이경우 튜플 타입이고, 읽기 전용이다 보니 다음과 같은 케이스에서 호환이 안되고 에러를 호출한다.
let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point); // 튜퓰 [3,4] 지 타입 , [number, number]가 아니다
// Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
// The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.