한 범위안의 변수가 null인 경우와 그렇지 않은 경우보다, 모두가 null이거나 전부 null이 아닌 경우로 분명히 구분하는것이 쉽다.
타입에 null을 추가하는 방식으로 이런 경우를 모델링 할 수 있는데
// tsConfig: {"strictNullChecks":false}
function extent(nums: number[]) {
let min, max
for (const num of nums) {
if (!min) {
min = num
max = num
} else {
min = Math.min(min, num)
max = Math.max(max, num) // 이 부분에서 에러
}
}
return [min, max]
}
-> 코드의 문제점은 min의 값만 null 체크를 하기 때문에 max 부분에서 number | undefined 형식의 인수가 number에 할당될 수 없다고 나온다.
// tsConfig: {"strictNullChecks":true}
function extent(nums: number[]) {
let min, max
for (const num of nums) {
if (!min) {
min = num
max = num
} else {
min = Math.min(min, num)
max = Math.max(max, num)
//~~'number | undefined' 형식의 인수는
//'number' 형식의 매개변수에 할당될 수 없다.
}
}
return [min, max]
}
-> strictNullChecks 설정을 켜면 문제점은 extent의 반환값이 (number | undefined)[]로 추론되어 설계적 결함이 분명해져 extent를 호출하는 곳마다 타입 오류가 발생한다.
const [min, max] = extent([0, 1, 2])
const span = max - min
// ~~~ ~~~ Object is possibly 'undefined'
extent 함수의 오류는 undefined를 min에서만 제외했고 max에서는 제외하지 않았기 때문에 발생한 오류다.
두 개의 변수는 동시에 초기화되지만, 이런 정보는 타입 시스템에서 표현할 수 없다. max에 대한 체크를 추가해 오류를 해결할 수도 있지만 버그가 더 늘어날 것이다.
문제를 해결하기 위해 단일 객체 사용하고, 함수호출의 결과값을 단언 혹은 if 구문으로 체크하면 된다.
const extent = (nums: number[]) => {
let result: [number, number] | null = null;
for (const num of nums) {
if (!result) {
result = [num, num];
} else {
result = [Math.min(num, result[0]), Math.max(num, result[1])];
}
}
return result;
};
null을 섞어서 사용하는 경우가 클래스에서 문제가 될수 있는데, 값이 완전히 준비되지 않은 상태에서 네트워크 요청을 하는 경우 그 사이의 속성들의 값이 null이냐 아니냐에 따라서 불확실성이 생긴다.
ts에서 유니온 타입(Union Type)은 여러 타입 중 하나를 나타내는 타입으로, | 연산자를 사용하여 표현된다. 예를 들어, 다음과 같이 선언할 수 있다.
type MyType = number | string;
이렇게 선언된 MyType은 숫자 또는 문자열 타입 중 하나를 나타낸다.
반면에 인터페이스(Interface)는 객체의 타입을 정의하는데 사용된다. 예를 들어, 다음과 같이 선언할 수 있다.
interface Person {
name: string;
age: number;
}
이렇게 선언된 Person 인터페이스는 이름과 나이라는 두 개의 속성을 가진 객체의 타입을 정의한다.
따라서, 인터페이스의 유니온을 사용하면 다양한 속성을 가진 객체의 타입을 정의할 수 있다. 예를 들어, 다음과 같이 선언할 수 있습니다.
interface Shape {
kind: Square | Rectangle | Circle;
}
요롷게 유니온의 인터페이스로 작성하는 것 보다는
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
이렇게 선언된 Shape 타입은 Square, Rectangle, Circle 인터페이스의 유니온으로 정의하는게 잘못된 조합으로 섞이는 경우를 방지할 수 있다. 또한 안에 태그된 유니온이 있으면 태그를 참고하여 Shape의 타입의 범위를 좁힐 수도 있다.
이를 통해, 각각의 도형에 해당하는 속성을 가진 객체를 만들 수 있는데
const square: Shape = { kind: "square", size: 10 };
const rectangle: Shape = { kind: "rectangle", width: 10, height: 20 };
const circle: Shape = { kind: "circle", radius: 5 };
따라서, 인터페이스의 유니온을 사용하면 코드의 가독성을 높이고, 타입 안정성을 유지하면서 다양한 타입을 정의할 수 있다.
선택적 필드가 여러개 있는 경우엔 두 개의 속성을 하나의 객체로 모아 그 객체가 존재하는지 체크하는 방식으로 사용하면 편리하다. 또한 타입의 구조를 손 댈 수 없는 API의 결과 상황에서 인터페이스의 유니온을 사용해 관계를 모델링할 수 있다.