docs(TechLog): 주제 4·5 를 스킬대로 다시 쓴다
what-the-compiler-lets-through 중앙값 2,096 → 2,590 자 seams-no-test-crosses → 3,091 자 bivariance 가 왜 느슨한 판정을 받는지, never 캐스트가 왜 아무것도 요구하지 않는지처럼 「이름을 댔으면 왜 있는지도 댄다」를 채웠다. 네 가지가 각각 무엇을 통과시키고 어디서 드러났는지를 표로 갈랐다. 검사 넷 중 무엇이 SQL 을 실제로 돌리는지도 표로 세웠다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a8ce0dda07
commit
53537e37e8
+27
-10
@@ -31,17 +31,25 @@ source:
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
## 검사가 무엇을 묻는가
|
||||
|
||||
「타입 검사가 통과했다」가 뜻하는 것은 컴파일러가 물은 질문에 코드가 답했다는 것이다. 이 저장소에서 네 번, 컴파일러가 물었어야 할 질문을 묻지 않았다.
|
||||
|
||||
넷은 서로 다른 방식으로 질문을 바꾼다. bivariance 는 「이 구현이 그 포트와 맞는가」를 「두 시그니처가 호환되는가」로, `as` 는 「이 이름이 그 타입에 있는가」를 작성자의 선언으로, `never` 캐스트는 「이 인자를 다 적었는가」를 검사 없음으로, 검사 대상이 없는 tsconfig 는 「이 코드가 컴파일되는가」를 대상 없음으로 바꾼다.
|
||||
|
||||
## 메서드 매개변수는 bivariant 다
|
||||
|
||||
포트가 네 값을 받는다고 선언하고 구현이 세 값만 적어도 두 시그니처는 호환된다고 판정된다. TypeScript 는 메서드 문법으로 쓴 매개변수를 bivariant 로 다룬다.
|
||||
포트가 네 값을 받는다고 선언하고 구현이 세 값만 적어도 두 시그니처는 호환된다고 판정된다.
|
||||
|
||||
```ts
|
||||
deleteDocument(kind: "CASE" | "REFERENCE" | "QUESTION" | "CONCEPT", id: string): Promise<void>;
|
||||
```
|
||||
|
||||
구현이 이 시그니처를 만족한다고 통과해도, 실행할 때 네 번째 값이 어디로 가는지는 구현 안의 분기가 정한다.
|
||||
메서드 문법으로 쓴 매개변수를 TypeScript 가 bivariant 로 다루기 때문이다. 매개변수를 엄격하게 반변으로 따지면 콜백을 넘기는 흔한 패턴이 대부분 거절되므로, 언어가 이 판정을 느슨하게 둔다.
|
||||
|
||||
같은 이유로 포트와 어댑터가 타입을 따로 들고 있으면 `satisfies` 도 한쪽이 좁아진 것을 잡지 못한다.
|
||||
느슨한 판정 아래에서는 구현이 좁게 적혀 있어도 넓은 포트를 만족한 것이 된다. 네 번째 값이 어디로 가는지는 구현 안의 분기가 정하고, 그 분기에 그 값이 없으면 마지막 `else` 로 간다.
|
||||
|
||||
같은 이유로 포트와 어댑터가 타입을 따로 들고 있으면 `satisfies` 도 한쪽이 좁아진 것을 잡지 못한다. 타입을 하나로 합치면 한쪽만 늘어날 수 없다.
|
||||
|
||||
## as 단언은 이름을 묻지 않는다
|
||||
|
||||
@@ -49,22 +57,31 @@ deleteDocument(kind: "CASE" | "REFERENCE" | "QUESTION" | "CONCEPT", id: string):
|
||||
const summary = body.purposeSummary as string; // 계약에 그런 칸이 없다
|
||||
```
|
||||
|
||||
`as` 는 「이 값을 이 타입으로 다루겠다」는 선언이라, 그 이름이 응답 타입에 있는지 묻지 않는다. 실행하면 `undefined` 가 나온다.
|
||||
`as` 는 「이 값을 이 타입으로 다루겠다」는 선언이다. 그 이름이 응답 타입에 있는지를 컴파일러가 묻지 않고, 실행하면 `undefined` 가 나온다.
|
||||
|
||||
객체를 배열로 읽고 `.filter` 를 부르면 매핑이 통째로 터지는데, `as` 캐스트가 그 어긋남을 타입 검사에서 가린다.
|
||||
모양이 다른 경우에는 더 나빠진다. 객체를 배열로 읽고 `.filter` 를 부르면 매핑이 통째로 터지는데, 캐스트가 그 어긋남을 타입 검사에서 가리므로 배포된 뒤에 알게 된다.
|
||||
|
||||
## never 로 받으면 아무것도 요구하지 않는다
|
||||
|
||||
요청을 만드는 조립기가 입력을 `(input: never)` 로 받아 캐스팅하면, 계약에 인자를 더해도 컴파일러가 아무 말도 하지 않는다. 조립기가 손으로 나열한 질의 인자에 그 값이 없으면 요청에서 조용히 빠진다.
|
||||
요청을 만드는 조립기가 입력을 `(input: never)` 로 받아 캐스팅하면, 계약에 인자를 더해도 컴파일러가 아무 말도 하지 않는다. `never` 는 어떤 값도 속하지 않는 타입이라 무엇을 넘겨도 검사할 것이 없다.
|
||||
|
||||
목록의 페이지 번호를 눌러도 쪽이 넘어가지 않던 것이 이 모양이었다. `page` 가 조립기의 목록에 없었다.
|
||||
조립기가 손으로 나열한 질의 인자에 그 값이 없으면 요청에서 조용히 빠진다. 목록의 페이지 번호를 눌러도 쪽이 넘어가지 않던 것이 이 모양이었다 — `page` 가 조립기의 목록에 없었다.
|
||||
|
||||
## 검사할 파일이 없는 tsconfig
|
||||
|
||||
루트 tsconfig 가 `"files": []` 에 project references 만 나열하면 `npx tsc --noEmit` 은 한 파일도 검사하지 않고 성공한다. references 를 따라가는 것은 별도 명령이다.
|
||||
루트 tsconfig 가 `"files": []` 에 project references 만 나열하면, 그 파일을 직접 부르는 명령은 한 파일도 검사하지 않고 성공한다. references 를 따라가려면 그 모드로 부르는 별도 명령이 필요하다.
|
||||
|
||||
## 네 가지가 공통으로 하는 일
|
||||
이 상태에서 통과가 확인해 준 것은 「루트 tsconfig 가 유효하다」까지다.
|
||||
|
||||
넷 다 「이 코드가 그 타입과 맞는가」라는 질문을 다른 질문으로 바꾼다. bivariance 는 시그니처 호환으로, `as` 는 작성자의 선언으로, `never` 는 검사 없음으로, 빈 tsconfig 는 대상 없음으로 바꾼다.
|
||||
## 네 가지가 통과시킨 것
|
||||
|
||||
| 무엇이 통과했나 | 무엇이 확인되지 않았나 | 어디서 드러났나 |
|
||||
|---|---|---|
|
||||
| bivariance | 구현이 네 번째 값을 다루는가 | 배포본의 서버 로그 404 |
|
||||
| `as` 단언 | 그 이름이 응답 타입에 있는가 | 공개 화면이 비어 있음 |
|
||||
| `never` 캐스트 | 새 질의 인자를 조립기가 적었는가 | 페이지 번호가 안 넘어감 |
|
||||
| 검사 대상 없는 tsconfig | 이 코드가 컴파일되는가 | 운영의 ReferenceError |
|
||||
|
||||
넷 다 실행하거나 배포해야 드러났다. 그래서 반영의 증거는 타입 검사가 아니라 그 값의 여정 끝이다.
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
Reference in New Issue
Block a user