Loading the catalog…
Loading the catalog…
본 문서는 Claude를 사용해 정리했습니다. 『실무로 통하는 타입스크립트』 정확히 하나만, 전부, 또는 전무 — ExactOne과 AllOrNone 정확히 1개만 허용하거나, 모두 허용하거나, 아무것도 허용하지 않아야 할 때가 있다. 접근 : ExactOne<T> , AllOrNone<T, K> 를 만든다. 선택형 never 기법 을 사용한다. 허용하지 않는 나머지 프로퍼티는 모두 선택형으로 설정하고 값은 never 로 설정한다. never 와 호환되는 값은 없으므로 프로퍼티를 만들 수 없다. 선택형 never 기법의 원리 type Test1 = { a?: never }; const t1: Test1 = {}; // ○ — 프로퍼티가 없으면 통과 const t2: Test1 = { a: undefined }; // ○ — undefined는 넣을 수 있음 const t3: Test1 = { a: 1 }; // ✕ — 값이 있으면 막힘 ? 는 "프로퍼티가 아예 없어도 된다"는 뜻이라, 빈 객체는 통과한다. 다만 그 프로퍼티에 실제 값을 넣으려 하면, never 는 "존재할 수 없는 값의 타입"이라 어떤 값도 대입할 수 없어서 막힌다. ? 를 지우면 에러가 나는 이유 type Test2 = { a: never }; const t: Test2 = {}; // ✕ TS2741: Property 'a' is missing ? 가 없으면 그 프로퍼티는 필수 가 된다. "반드시 있어야 하는데, 넣을 수 있는 값이 never (즉 없음)"라는 모순이 생겨서, 빈 객체조차 이 타입을 만족시킬 방법이 없다. 그래서 아무것도 대입할 수 없는 타입이 되어버린다. ? 가 있어야 "이 프로퍼티는 아예 생략 가능하다"는 탈출구가 생겨서 실용적으로 쓸 수 있다. ExactOne — 프로퍼티 1개만 허용 type ExactOne<T> = { [K in keyof T]: { [P in K]: T[P]; } & { [P in Exclude<keyof T, K>]?: never; }; }[keyof T]; type VideoFormatURLs = { format360p: URL; format480p: URL; format720p: URL; format1080p: URL; }; function loadVideo(format: ExactOne<VideoFormatURLs>) {} loadVideo({ format360p: new URL("") }); // ○ loadVideo({ format360p: new URL(""), format480p: new URL(""), }); // ✕ 각 키마다 "그 키만 값을 가짐 + 나머지는 전부 선택형 never "인 타입을 만들고, [keyof T] 로 인덱싱해서 유니온으로 합친다. 어떤 멤버를 선택하든 그 멤버가 아닌 프로퍼티에는 값을 넣을 수 없으니, 정확히 하나만 허용된다. ExactOne과 exactOptionalPropertyTypes loadVideo({ format360p: new URL(""), format480p: undefined, }); exactOptionalPropertyTypes 설정에 따라 결과가 갈린다. 설정 결과 꺼짐 (기본값) 통과된다. ?: never 가 never | undefined (= undefined )를 허용하기 때문 켜짐 막힌다. ? 가 "키를 생략할 수 있다"는 뜻으로 엄격해져서, undefined 값 자체도 그 자리에 넣을 수 없어진다 직접 확인한 결과, --exactOptionalPropertyTypes 플래그를 켜면 정확히 TS2379: ... Type 'undefined' is not assignable to type 'never' 에러가 난다. 선택형 never 기법을 신뢰도 있게 쓰려면 exactOptionalPropertyTypes: true 를 함께 켜는 것이 사실상 필수다. 이 옵션 없이는 undefined 를 명시적으로 대입하는 경로로 "정확히 하나만"이라는 보장이 우회될 수 있다. Split으로도 만들 수 있다 type Split<T, OptionalNever extends boolean = false> = { [K in keyof T]: { [P in K]: T[P]; } & (OptionalNever extends false ? {} : { [P in Exclude<keyof T, K>]?: never; }); }[keyof T]; type ExactlyOne1<T> = Split<T, true>; OptionalNever 플래그로 "최소 1개"(기본값, {} )와 "정확히 1개"( true , 나머지를 never 로 막음) 두 가지를 하나의 헬퍼로 표현한다. AllOrNone — 지정한 키들을 전부 쓰거나 전부 안 쓰거나 type AllOrNone<T, Keys extends keyof T> = ( | { [K in Keys]-?: T[K] } // 모두 사용 가능 (필수로 강제) | { [K in Keys]?: never } // 모두 사용 불가능 ) & { [K in Exclude<keyof T, Keys>]: T[K]; // 나머지는 정의된 대로 }; -? 는 선택형 표시를 강제로 지워 필수로 만드는 매핑 타입 변경자다. 유니온의 두 멤버가 "지정한 키들이 전부 필수"이거나 "전부 never "인 두 극단만 표현해서, 일부만 쓰는 중간 상태를 막는다. -?의 실제 효과 — 있어도 없어도 같을 수 있다 type WithoutMinusQ = { [K in Keys]: T[K] }; // a 필수 type WithMinusQ = { [K in Keys]-?: T[K] }; // a 필수 (동일) Keys 가 이미 별도 제네릭 매개변수라서 비동형 매핑이고, 그래서 -? 없이도 이미 필수로 나온다. 직접 확인한 결과, 이 경우 -? 가 있든 없든 결과가 완전히 같았다. 기능적으로는 중복이다. 그런데도 -?를 쓰는 이유 — 의도를 명시하는 방어적 표기 type AllOrNone<T, Keys extends keyof T> = ( | { [K in Keys]-?: T[K] } // "이건 반드시 필수여야 한다"는 의도를 코드로 명시 | { [K in Keys]?: never } ) & { ... }; Keys 가 별도 매개변수를 거쳐서 "우연히" 필수가 된다는 사실은 코드를 처음 보는 사람 입장에서 당연하지 않다. -? 를 명시적으로 써두면 "어쩌다 필수인 게 아니라 의도적으로 필수로 만든 것"이라는 게 코드만 봐도 드러난다. 나중에 Keys 가 keyof T 로 직접 대체되는 리팩터링이 일어나도 안전하게 동작하도록 지켜주는 역할도 한다. -?가 실제로 필요해지는 경우 keyof T 를 직접 쓴 동형 매핑이라면 얘기가 달라진다. type ForceRequired<T> = { [K in keyof T]-?: T[K] }; 이 경우엔 -? 가 없으면 원본의 ? 가 자동으로 유지되어버리므로, -? 가 실제로 효과를 내는 유일한 경우 가 된다. Required<T> 의 실제 정의도 이 형태다. type Required<T> = { [P in keyof T]-?: T[P] }; 동형 vs 비동형 매핑 타입 동형(homomorphic) : [K in keyof T] 처럼 keyof T 를 직접 써서 순회하는 매핑. 원본 T의 ? (선택성), readonly 가 자동으로 그대로 복사된다. type T = { a?: string; readonly b: number }; type Copy = { [K in keyof T]: T[K] }; // { a?: string; readonly b: number } ← 원본 그대로 유지 비동형(non-homomorphic) : keyof T 가 아니라 별도 이름(제네릭 매개변수 등)을 거쳐 순회하는 매핑. 값이 keyof T 와 같아도 T와의 연결이 끊겨, ? 와 readonly 가 복사되지 않고 기본값(필수, 수정 가능)으로 초기화된다. type ViaKeys<Keys extends keyof T> = { [K in Keys]: T[K] }; type Result = ViaKeys<"a" | "b">; // { a: string; b: number } ← ?와 readonly 둘 다 사라짐 기준 : 매핑 타입이 순회하는 자리에 keyof T 가 직접 적혀 있는가(동형), 아니면 다른 이름을 거쳐 있는가(비동형). 정리 매핑 방식 원본의 ? 유지 여부 -? 의 효과 [K in keyof T] (동형) 유지됨 실제로 필요 — 강제로 필수화할 때 [K in Keys] (Keys는 별도 매개변수, 비동형) 유지 안 됨 (기본 필수) 중복 — 의도 명시용 방어적 표기 AllOrNone2 — 최소 1개 조건까지 추가 type AllOrNone2<T, Keys extends keyof T> = ( | { [K in Keys]-?: T[K] } | { [K in Keys]?: never } ) & Split<T>; // 1개 이상의 프로퍼티가 필수 function loadVideo2( format: AllOrNone2<VideoFormatURLs, "format360p" | "format480p">, ) {} loadVideo2({ format360p: new URL(""), format480p: new URL(""), }); // ○ — 지정한 키 둘 다 있음 loadVideo2({ format1080p: new URL("") }); // ○ — 지정한 키는 없고 다른 키가 있음 loadVideo2({ format480p: new URL(""), format1080p: new URL(""), }); // ✕ — 지정한 키 중 하나만 있음 직접 검증한 결과, 세 케이스 모두 의도대로 동작했다. format360p 와 format480p 는 "함께 있거나 둘 다 없거나"만 허용되고, 나머지 포맷( format1080p 등)은 Split<T> 덕분에 최소 1개는 있어야 한다는 조건이 함께 적용된다. AllOrNone3 — 내장 헬퍼 타입으로 구현 type AllOrNone3<T, Keys extends keyof T> = ( | Required<Pick<T, Keys>> | Partial<Record<Keys, never>> ) & Split<T>; Required<Pick<T, Keys>> 가 "지정한 키들을 전부 필수로", Partial<Record<Keys, never>> 가 "지정한 키들을 전부 선택형 never 로"에 대응한다. 직접 만든 매핑 타입과 같은 결과를 내장 유틸리티 조합으로 표현한 것이다. 유니온을 인터섹션으로 변환하기 — UnionToIntersection 다른 타입을 파생하려면 유니온 타입을 인터섹션 타입으로 먼저 변환해야 할 때가 있다. type BasicVideoData = { /* 구현 중 */ }; type Format320 = { urls: { format320p: URL } }; type Format480 = { urls: { format480p: URL } }; type Format720 = { urls: { format720p: URL } }; type Format1080 = { urls: { format1080p: URL } }; // 한 가지 이상의 포맷을 반드시 정의하도록 요구하는 타입 type Video = BasicVideoData & (Format320 | Format480 | Format720 | Format1080); 원문 노트의 Format320 ~ Format1080 정의가 전부 { urls: { format320p: URL } } 로 똑같이 적혀 있었다. 확인해보니 그래서 네 타입이 완전히 동일한 타입이 되어 유니온에서 하나로 합쳐졌고, keyof Video["urls"] 가 never 가 아니라 "format320p" 하나만 나온 것이었다. 각 타입의 프로퍼티 이름을 실제 포맷에 맞게( format480p , format720p , format1080p ) 고치면 이 문제가 해소된다. type FormatKeys = keyof Video["urls"]; // 원문 의도: "모든 포맷 키의 유니온"이어야 하는데, 위 복붙 실수 때문에 "format320p" 하나만 나왔었다. type Video2 = BasicVideoData & { urls: { format320p: URL; format480p: URL; format720p: URL; format1080p: URL; }; }; // 모든 키의 유니온 타입이 나온다 type FormatKeys2 = keyof Video2["urls"]; 하지만 Video (유니온 버전)처럼 인터섹션으로 묶인 여러 객체 타입에서, 각 타입 안의 프로퍼티 이름들을 유니온으로 뽑아내려면, 먼저 유니온을 인터섹션으로 바꿔야 한다. type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends ( x: infer R, ) => any ? R : never; 작동 순서 UnionToIntersection<string | number> 를 예로 든다. 1단계 — 네이키드 T를 함수 매개변수로 감싸며 분배시킨다 T extends any ? (x: T) => any : never T 가 네이키드 타입 매개변수 (조건부 타입의 검사 대상에 아무것도 감싸지 않고 그대로 놓인 상태)라서, 유니온이 각 멤버로 쪼개져 개별적으로 처리된 뒤 다시 유니온으로 합쳐진다. 이를 분배 조건부 타입(distributive conditional type) 이라 한다. = (T extends any ? (x:T)=>any : never) 를 T=string, T=number 각각에 적용 = ((x: string) => any) | ((x: number) => any) T extends any 는 항상 참이라 : never ( false 분기)는 사실상 도달하지 않는 죽은 코드다. TypeScript의 조건부 타입 문법이 T extends U ? X : Y 삼항 형태만 존재하고 Y 없이 X 만 쓰는 문법이 없어서, 의미 없는 자리라도 채워야 한다. 그 자리에 "실행되지 않는다"는 의도를 가장 잘 드러내는 never 를 관례적으로 넣는다. 실제로 이 자리에 다른 타입을 넣어도 최종 결과는 달라지지 않는다. 2단계 — 함수 유니온을 다시 하나의 조건부 타입에 (이번엔 감싸지 않은 채로) 넣는다 (위에서 만든 함수 유니온) extends (x: infer R) => any ? R : never 이번에 검사 대상은 T 가 아니라 1단계에서 이미 만들어진 "함수 타입들의 유니온" 자체 다. 이건 네이키드 타입 매개변수가 아니라 이미 계산이 끝난 하나의 구체적인 타입이라서, 더 이상 분배되지 않고 통째로 검사된다. 3단계 — 함수 매개변수의 반변성이 유니온을 인터섹션으로 뒤집는다 함수 타입의 매개변수 자리는 반변(contravariant) 이다. 공변과 반변 — UnionToIntersection과의 연결 공변(covariant) "안의 타입이 좁으면(하위 타입이면), 바깥 타입도 좁다(하위 타입이다)"는 방향이 그대로 유지되는 것이다. string 은 string | number 의 하위 타입 (더 좁음) ↓ 배열로 감싸도 방향 유지됨(공변) string[] 은 (string|number)[] 의 하위 타입 (더 좁음) string 이 string | number 보다 좁은 것처럼, 배열로 감싸도 그 좁고 넓은 관계가 그대로 유지된다. 이게 "공변"이라는 말의 뜻이다 — "함께(co) 변한다(vary)", 즉 안쪽 타입의 방향과 바깥 타입의 방향이 같은 쪽으로 움직인다. 반변과 나란히 비교하면 안쪽 관계 바깥 타입에서의 관계 방향 배열 (공변) string ⊂ string|number string[] ⊂ (string|number)[] 같은 방향 유지 함수 매개변수 (반변) string ⊂ string|number (x: string|number)=>void ⊂ (x: string)=>void 방향이 뒤집힘 배열은 안쪽이 좁으면 바깥도 좁다. 하지만 함수 매개변수는 안쪽이 넓으면(더 많은 타입을 받으면) 바깥(그 함수 자체)이 오히려 더 좁은 타입(더 구체적인, 더 유능한 함수)이 된다. 이게 공변과 반변의 결정적 차이다. UnionToIntersection에서 쓰인 것과 연결하면 바로 이 "반변" 성질 때문에 함수 매개변수 자리에서는 유니온이 인터섹션으로 뒤집혔던 것이다. (x: string | number) => void ← 이게 (x: string) => void 의 하위 타입 (반변이라 뒤집힘) string | number (넓음)를 받는 함수가, string (좁음)만 받는 함수보다 더 좁은 (더 구체적인, 더 제약이 많은) 타입으로 취급된다. 배열이었다면 정반대였을 것이다. ((x: string) => any) | ((x: number) => any) 라는 함수 유니온을 만족하는 단일 함수 타입 을 찾으려면, 그 함수는 string 도 받을 수 있고 number 도 받을 수 있어야 한다 — 즉 매개변수 타입이 string 이면서 동시에 number 인 것 , 다시 말해 string & number 여야 한다. 반변 위치에서 여러 함수 타입을 하나로 합칠 때는 매개변수 자리의 유니온이 인터섹션으로 뒤집힌다. 4단계 — infer R이 그 인터섹션을 뽑아낸다 (x: infer R) => any R 은 정확히 3단계에서 계산된 "매개변수 자리의 인터섹션 타입"이 된다. UnionToIntersection<string | number> 의 결과는 string & number 가 된다. 객체 타입에 적용하면( { a: string } | { b: number } ) 결과는 { a: string } & { b: number } 가 되어, 양쪽 프로퍼티를 모두 가진 값만 통과하는 타입이 만들어진다. 정리하면 전체 트릭은 이렇다. 1. T(유니온)를 네이키드 자리에 놓아 분배시켜서, "함수들의 유니온"을 만든다 2. 그 함수 유니온을 다시 하나의 조건부 타입에 (감싸지 않은 채로) 넣는다 → 이번엔 이미 유니온이 아니라 "함수 타입 하나"로 취급되어 분배되지 않는다 3. 함수 매개변수는 반변이라, 여러 함수를 동시에 만족하는 단일 함수를 찾으면 매개변수 자리에서 유니온이 인터섹션으로 뒤집힌다 4. infer R로 그 인터섹션을 뽑아낸다 "유니온을 인터섹션으로 뒤집는" 효과를 내는 유일한 표준 메커니즘이 함수 매개변수의 반변성이기 때문에, 굳이 함수 타입으로 감싸는 이 우회로를 쓴다. 객체 프로퍼티나 배열 요소 위치는 공변이라, 그 자리에서는 유니온이 유니온인 채로 남는다.
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
2026.09.30 1일 1로그. 본 문서는 Claude를 사용해 정리했습니다. 『실무로 통하는 타입스크립트』 정확히 하나만, 전부, 또는 전무 — ExactOne과 AllOrNone 정확히 1개만 허용하거나, 모두 허용하거나, 아무것도 허용하지 않아야 할 때가 있다. 접근 : ExactOne , AllOrNone 를 만든다. 선택형 never 기법 을 사용한다. 허용하지 않는 나머지 프로퍼티는 모두 선택형으로 설정하고 값은 never 로 설정한다. never 와 호환되는 값은 없으므로 프로퍼티를 만들 수 없다. 선택형 never 기법의 원리 type Test1 = { a?: never }; const t1: Test1 = {}; // ○ — 프로퍼티가 없으면 통과 const t2: Test1 = { a:…
Open source