
컴포넌트 props로 값을 넘길때 props명과 변수,함수명이 동일하다면 구조분해 후 객체 재생성으로 이용해서 넘겼다. 그런데 PR리뷰어가 이 방식으로 넘기면 props에 없는 변수를 넘겨도 에러가 뜨지 않으니 삭제를 요구했다.
return <MyComponent
{...{
name,
age,
width:1 // MyComponentProps에 없는 속성이지만 에러가 안뜸
}
}
/>
왜 안뜨지????🧐 그래서 관련 개념들을 찾아보다 보니 구조적타입을 만나게 됐다.
타입스크립트는 구조적 타입시스템이다
구조적 타입시스템은 구조적 타이핑과 구조적 서브타이핑을 일컫는 개념이다.
구조적 서브타이핑을 알기 전에 구조적 타이핑에 대해서 알아보겠다.
구조적 타이핑은 타입의 속성들만으로 타입을 연결시킨다.
즉 서로 다른 이름을 가진 타입들이 동일한 속성을 갖는다면 같은 타입으로 인지한다는 의미이다.
이를 duck Type이라고도 한다.
dock Type
오리처럼 생겼다면 그것은 오리이다
거위가 오리와 같은 모든 속성을 가지고 있다면 그것은 또한 오리이다.
Ball과 Sphere은 서로 다른 타입이지만 구조와 속성이 동일하기에 Typescript는 동일한 타입으로 인지가 가능하다.
type Ball ={
diameter: number;
}
type Sphere ={
diameter: number;
}
let ball: Ball = { diameter: 10 };
let sphere: Sphere = { diameter: 20 };
sphere = ball; // OK
ball = sphere; // OK
비교하는 타입들 중 하나의 타입이 다른 하나의 타입의 속성을 모두 가지고 있다면 타입스크립트는 동일한 타입이라고 인지한다.
위와 같은 개념이 있기때문에 아래와 같은 상황들을 마주했을 것이다
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
const point1 = { x: 12, y: 26 };
logPoint(point); // logs "12, 26"
const point2 = { x: 12, y: 26, z: 89 };
logPoint(point2); // logs "12, 26"
const color = { hex: "#187ABF" };
logPoint(color); // error: Argument of type '{ hex: string; }'
is not assignable to parameter of type 'Point'.
point1
point2
color
위 내용은 변수에 대해 타입정의를 해도 동일한 결과를 나타낸다.
함수의 매개변수로 넘기기 위해 바로 생성한 *객체리터럴은 왜 안되는 것일까요? 🤷♂️
*객체 리터럴
자바스크립트에서 객체를 선언하는 일반적인 방식으로 key,value값을 바로 넣는 것이다.
ex) const Point = { x:1, y:2}
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
logPoint({x:1,y:2,width:9}); // Object literal may only specify
known properties, and 'width' does not exist in type 'Point'.
에러메세지를 해석하면 객체리터럴은 지정된 속성만 사용할 수 있다고 한다.
(여긴 구조적 서브타이핑이 적용이 안된다니??)
이와 관련된 Typescript_GitHubPR을 해석해보았다.
타입스크립트 개발자가 말하길 객체리터럴로 직접 매개변수로 넘기는 경우 잘못된 이름의 속성 및 불필요한 속성을 과다하게 넘기는 것을 방지하기 위해 객체 리터럴 할당을 엄격하게 제한하겠다
Q: 어떻게 제한하나?
A: 신선한 객체 리터럴인지를 파악하겠다!
모든 객체 리터럴은 처음에는 신선(fresh) 하다.
신선한(fresh) 경우
-> 신선한 객체리터럴은 무조건 타입에 맞춰서 써야한다 즉 엄격한 타입검사를 하겠다.
type Point={
name:number;
age:number;
}
const Fresh_Object_Literal:Point={
name:20,
age:10
} // OK
const Fresh_Object_Literal_Error:Point={
name:20,
age:10,
height:12
} // Error
//test함수의 매개변수 타입이 Point
test({name:20,age:10}) // OK
test({name:20,age:10,width:9}) // Error
신선함이 없는(freshness) 경우
-> 신선하지 않은 객체 리터럴은 엄격한 타입검사를 하지 않겠다
type NotPoint={
name:number;
age:number;
width:number;
}
//test함수의 매개변수 타입이 Point
test({name:4,age:1,width:9} as NotPoint) // OK
test({name:4,age:1,height:9} as Point & {height:number}) // OK
신선한 객체리터럴은 엄격한 타입검사를 하기 때문에 매개변수로 객체리터럴을 넘기는 경우는 타입이 꼭 맞아야 한다!
아래와 같이 Props타입에 존재하지 않는 속성값을 객체에 넣고 이를 구조분해 후 객체 재생성으로 넘겨도 에러가 뜨지 않음을 알 수 있다.
interface Props {
name: string;
age: number;
}
const MyComponent=(props:Props)=>{
const {name,age}=props;
console.log(name,age)
}
const YouComponent=()=>{
const name = "wooju";
const age = 100
return (
<>
// 신선한 객체 리터럴임으로 Error
<MyComponent name={"wooju"} age={100} job={"dev"} />
// 객체에 대한 타입추론으로 문제없기에 OK
<MyComponent
{...{
name,
age,
job : 1 // no error
}}
/>
</>
)
}
구조분해 후 새로운 객체가 만들어진 경우는 별도로 타입을 정의하지 않았기때문에
타입스크립트는 새로운 객체의 타입을 단순히 아래처럼 추론한다.
{name:string; age:number; job:number}
따라서 MyComponent측에서는 MyComponent 타입에 있는 값이 전부 있는지만 판단되면 동일한 타입으로 인지해서 에러가 뜨지 않는다.
명목적 타입 시스템은 구조타입시스템과는 다르게 동일한 속성을 가진 타입이라고 한들 정의된 이름이 다르다면 다른 타입으로 인지한다.
예시) C#,Java
타입 정의할때 특정 타입을 명시적으로 상속한 경우에만 타입 호환을 하는 방식
type ParentType={name:string}
type ChildType= ParentType & {age:number}
let child:ChildType
parentFunc(child) // OK
타입스크립트의 구조적 타입시스템은 자바스크립트 방식을 기반으로 만들어졌다.
자바스크립트는 함수 표현식이나 객체 리터럴과 같은 익명 객체를 광범위하게 하는데 이런 관계를 나타내기 위해 구조적 타입 시스템을 사용하는 것이 명목적 타입 시스템을 사용하는 것보다 훨씬 더 자연스럽다.
결국, 자바스크립트에서는 객체의 이름이나 명시적인 타입 선언보다 그 객체의 구조(즉, 객체가 어떤 속성과 메서드를 가지고 있는지)가 더 중요하게 사용되므로, 타입스크립트는 이러한 자바스크립트의 특성을 반영하여 구조적 타입 시스템을 채택한 것입니다.
일을 하다보니 명확하지 않은 지식으로 개발을 진행하고 있음을 깨달았다. 앞으로는 좀 더 "왜?" 라는 질문을 던지면서 근본을 파헤쳐가며 일을 진행해야겠다는 반성을 했다.
Typescript_handbook_structuralTypeSystem
Typescript_Deep_Dive
vallista
toss_tech