Flow와 TypeScript의 공통점과 차이점, 그리고 Flow만의 개념과 TypeScript 전용 기능을 비교합니다.
Flow와 TypeScript는 대부분의 동일한 문법, 많은 동일한 용어, 그리고 큰 범위의 겹치는 개념들(조건부 타입, 매핑된 타입, 타입 가드, keyof, as const, unknown, Readonly)을 공유합니다. 지난 몇 년 동안 Flow의 문법은 TypeScript와 정렬되도록 변화해 왔습니다. TypeScript를 알고 있다면, 여러분의 직관만으로도 Flow 프로그램을 대부분 이해할 수 있습니다.
이것이 Flow일까요, 아니면 TypeScript일까요? 구분할 수 없습니다.
1type User = {2 readonly name: string,3 readonly age: number,4 readonly metadata: unknown,5};6
7function get<K extends keyof User>(8 user: User,9 key: K,10): User[K] {11 return user[key];12}13
14declare const user: User;15const age: number = get(user, 'age');
두 언어가 갈라지는 지점에서는, 그 차이가 대개 더 강한 정적 보장을 위한 Flow의 의도적인 선택입니다. Flow는 TypeScript가 허용하지만 런타임에서 예외를 던지거나, 값을 조용히 손상시키거나, 로직 버그를 일으킬 수 있는 여러 패턴을 거부합니다. 또한 Flow는 TypeScript에 내장된 대응 기능이 없는 여러 기능을 제공하며, 이는 아래의 Flow 전용 개념에서 다룹니다. 가장 두드러진 예는 React입니다. Flow는 자체적인 일급 component, hook, 그리고 renders 문법을 가집니다.
이 페이지는 네 개의 섹션으로 구성되어 있습니다.
추가 참고 섹션에서는 곧 지원될 TS 정렬 작업, 기존 Flow 문법 수렴, 공유 설정 옵션, 그리고 외부 선언 메커니즘을 다룹니다.
범위 참고:
tsc와 달리flow바이너리 자체는 타입 검사기일 뿐이며 JavaScript를 출력하지 않습니다. Flow 문법을 런타임 JS로 컴파일하는 일은 별도의 빌드 도구가 담당합니다. TypeScript는 이 둘을 하나의 도구에 함께 포함합니다. 아래의 TypeScript 관련 설명은strict를 활성화한 버전 6.0.3을 기준으로 검증되었습니다.
아래 기능들은 문법과 의미가 충분히 가깝기 때문에, TypeScript에 대한 직관을 거의 그대로 재사용할 수 있습니다.
infer를 포함한 조건부 타입, 그리고 infer T extends Bound 제약 형태 포함.-?를 포함한 매핑된 타입.param is T 형태의 타입 가드 (추가 검증 포함), 추론된 타입 프레디킷 포함.T[K] 인덱스 접근 타입.keyof T 연산자.as const 단언.const 타입 파라미터 - function f<const T>(x: T): T.unknown 최상위 타입.<T extends Bound>를 사용하는 제네릭 경계.Readonly, ReadonlyArray, ReadonlyMap, ReadonlySet, Pick, Omit, Record, Partial, Required, Exclude, Extract, NonNullable, Parameters, ReturnType, NoInfer, Awaited, ThisParameterType, OmitThisParameter, ConstructorParameters, InstanceType, Uppercase, Lowercase, Capitalize, Uncapitalize.import type와 export type.((x: number) => string) & ((x: string) => number)는 두 언어 모두에서 인자 타입에 따라 호출별 반환 타입을 해석합니다.declare const, declare let, declare class, declare function 같은 ambient 선언 형태.기저 의미는 다르지만 문법은 TypeScript와 직접 일치하는 두 가지 형태가 더 있습니다. 자세한 내용은 분산성을 보세요.
이 섹션은 TypeScript에서 온 독자에게 Flow가 가장 자주 놀라움을 주는 부분입니다. 두 언어 모두 이런 개념들(객체, 클래스, 분산성, 정제, 제네릭, 모듈 export, suppression)을 가지고 있지만, Flow의 규칙은 코드를 읽는 것만으로는 드러나지 않을 수 있는 방식으로 달라집니다.
대부분의 하위 섹션에서는 문법은 같지만 의미가 다릅니다. 즉, TypeScript에서는 받아들여지지만 Flow에서는 잘못되었거나, 건전하지 않거나, 안전하지 않다고 판단해 거부하는 코드들입니다. 흔히 이런 코드는 런타임에서 예외를 던지거나, 값을 조용히 손상시키거나, 로직 버그를 일으킵니다. 나머지는 이름만 다른 표기, Flow가 모듈 경계에서 추가하는 검증, 또는 다른 suppression 형태입니다. 각 하위 섹션 제목이 어떤 경우인지 알려줍니다.
readonly, writeonly, in, outvoid 대 undefined, ?T, empty, unknownas 캐스트, 오류 suppression, 타입 인자 생략명목적 타이핑과 구조적 타이핑.
TypeScript는 주로 구조적입니다. 공개된 모양이 같으면 두 타입은 서로 교환 가능합니다. 다만 좁은 범위의 명목적 예외(#private 필드, private/protected 수정자, unique symbol)가 있습니다. Flow는 일반 객체와 함수에 대해서는 구조적이지만, 클래스, opaque 타입, 그리고 Flow Enum에 대해서는 의도적으로 명목적 입니다.
Flow가 여기서 명목적 타이핑을 선택한 이유는, 이런 구성요소에서는 정체성 자체가 실제 정보를 담고 있기 때문입니다. 예를 들어 UserId와 PostId는 같은 내부 표현을 가질 수 있지만 서로 교환 가능하지는 않습니다. 정체성을 구조가 아니라 명목적으로 다루면 타입 시스템이 전체 범주의 로직 버그(모양은 맞지만 의미는 틀린 경우)를 잡아낼 수 있고, 사용자가 도메인을 단지 어떻게 생겼는가 가 아니라 무엇인가 의 수준에서 모델링할 수 있게 됩니다.
Flow가 객체, 클래스, 인터페이스를 타입화하는 방식: 기본 exact 객체 규칙, nominal 클래스 정체성, 비대칭적인 서브타이핑 규칙, 원시값의 인터페이스 할당 가능성, 타입 수준 spread, 클래스 메서드와 객체 리터럴의 this 바인딩 규칙, 그리고 튜플 spread.
| Surface | TypeScript | Flow |
|---|---|---|
| 객체 exactness | {x: number}는 신선한 객체 리터럴의 초과 속성 검사 외에는 추가 속성을 허용합니다. | {x: number}는 기본적으로 exact입니다. 추가 속성이 허용될 때는 {x: number, ...}를 사용합니다. |
| 클래스 / 인터페이스 / 객체 서브타이핑 | 클래스, 인터페이스, 객체 타입은 대체로 구조에 따라 상호 교환 가능합니다. | 클래스는 nominal이고, 객체 타입은 오직 객체 타입만 받으며, 인터페이스는 세 가지 모두 받을 수 있습니다 |
implements / extends 절 | Pick<T, K>나 Omit<T, K> 같은 객체 모양 유틸리티 타입을 대상으로 할 수 있습니다. | 객체 타입 별칭이 아니라 반드시 인터페이스나 클래스를 이름으로 지정해야 합니다. |
| 원시값 대 인터페이스 | 원시값은 boxed prototype에 존재하는 객체/인터페이스 모양을 만족합니다. | 원시값은 객체 타입이나 인터페이스의 서브타입이 아닙니다. |
| 객체 결합 | 객체 타입을 결합하는 표준 방법은 교차입니다. | 객체 타입 spread({...A, ...B})를 사용합니다. inexact 객체에 대해서는 교차도 지원되지만 exact 객체에는 동작하지 않습니다. |
this 바인딩 | 메서드 추출 시 안전하지 않게 this를 잃으며, 객체 리터럴 안의 this도 허용됩니다. | 클래스 메서드 추출은 거부되며, 객체 리터럴 안의 this는 금지됩니다. |
| 선택 요소 뒤의 튜플 spread | 허용됩니다. 결과 튜플 타입은 선택 요소가 없을 때의 런타임 레이아웃과 맞지 않습니다. | 원본 튜플의 arity가 정적으로 고정되지 않았으면 거부됩니다. |
Flow에서 객체 타입은 기본적으로 exact 입니다. {x: number}는 정확히 x 속성만 허용하고 그 외는 허용하지 않습니다. 추가 속성을 허용하려면 뒤에 ...를 붙인 inexact 형태 {x: number, ...}를 작성해야 합니다. TypeScript에서 객체 타입은 타입 시스템 수준에서 열려 있습니다. {x: number}는 추가 속성을 허용하며, 초과 속성을 잡아내는 규칙은 excess-property check 로서 직접적인 객체 리터럴 할당에서만 발동합니다.
이 차이는 중요합니다. TypeScript 코드가 겉으로는 Flow의 exactness와 같은 것처럼 보여도 실제로는 그렇지 않다는 점을 설명해 주기 때문입니다. const o: {x: number} = {x: 1, y: 2}는 TS에서도 오류가 나지만, 이는 리터럴이 인라인으로 들어가 있기 때문뿐입니다. 간접적인 경우(먼저 변수에 담은 뒤 할당, 함수에 통과시킴, 또는 리터럴의 "fresh" 상태가 사라지는 다른 경로)는 TS에서 타입 검사를 통과하며 거기서는 exactness 위반이 아닙니다. Flow의 exactness는 바인딩 형태와 무관하게 일관되게 적용되며, 아래 두 가지 TS 형태 패턴에서 드러납니다.
• 패턴: 간접 할당을 통해 추가 속성이 새어 들어간다.
// TypeScript:type Settings = { volume: number, brightness: number,};const raw = { volume: 0.5, brightness: 0.8, theme: "dark",};const settings: Settings = raw;Object.values(settings).map(v => v.toFixed(2), // Runtime crash! 0Settings0 types every value as 0number0, so 0.toFixed0 runs on the 0"dark"0 string);
TypeScript는 이것을 허용합니다. raw는 {volume: number, brightness: number, theme: string}으로 추론되며, TS의 객체 타입은 열려 있기 때문에 구조적으로 {volume: number, brightness: number}의 서브타입입니다. 초과 속성 검사는 간접 할당에서는 발동하지 않습니다. Flow는 같은 코드를 거부합니다.
1// Flow:2type Settings = {3 volume: number,4 brightness: number,5};6const raw = {7 volume: 0.5,8 brightness: 0.8,9 theme: "dark",10};11const settings: Settings = raw; // ERRORincompatible-typeCannot assign raw to settings because property theme is extra in object literal [1] but missing in Settings [2]. Exact objects do not accept extra props.
추가 속성이 의도적인 경우의 해결책은 inexact 형태입니다. 즉, {volume: number, brightness: number, ...}처럼 작성해 타입이 알 수 없는 추가 속성을 명시적으로 허용하도록 해야 합니다.
1// Flow:2type Settings = {3 volume: number,4 brightness: number,5 ...6};7const raw = {8 volume: 0.5,9 brightness: 0.8,10 theme: "dark",11};12const settings: Settings = raw; // OK
• 패턴: 어떤 속성을 잊어버린 뒤 다른 타입으로 다시 도입한다. 보통 더 느슨한 타입을 받고 선택 필드를 추가하는 TypeScript 시그니처를 Flow 함수로 모델링할 때 자주 나타납니다. TypeScript는 inexact 서브타이핑을 통해 속성을 잊어버린 뒤, 다른(선택적) 타입으로 다시 도입하는 것을 허용합니다. 그 결과 timestamp는 실제로는 문자열 '2026-01-15'를 담고 있는데도 number | undefined로 타입이 붙게 됩니다.
// TypeScript:type LogEntry = { id: string, timestamp: string,};type WithUnixTime = { id: string, timestamp?: number,};const e: LogEntry = { id: 'a', timestamp: '2026-01-15',};const base: {id: string} = e; // 0timestamp0 "forgotten"const widened: WithUnixTime = base; // 0timestamp0 re-introduced at a new typewidened.timestamp?.toFixed(); // Runtime crash! ?. only short-circuits on nullish; the value // is the string '2026-01-15', so .toFixed runs and throws.
Flow는 이 경로를 두 군데에서 막습니다. 직역한 형태는 forget 단계에서 실패합니다({id: string}이 기본적으로 exact이기 때문입니다). forget을 inexact 타깃으로 모델링하면 실패 지점이 re-introduce 단계로 이동합니다. exactness는 양방향 모두를 제한합니다. 속성은 타깃이 inexact일 때만 잊어버릴 수 있고, 다시 도입하는 것은 소스가 exact일 때만 가능합니다.
1// Flow:2type LogEntry = {3 id: string,4 timestamp: string,5};6type WithUnixTime = {7 id: string,8 timestamp?: number,9};10
11declare const e: LogEntry;12
13// Literal Flow translation: fails at the forget step because `{id: string}` is exact.14const baseExact: {id: string} = e; // ERRORincompatible-typeCannot assign e to baseExact because property timestamp is extra in LogEntry [1] but missing in object type [2]. Exact objects do not accept extra props.15
16// Modeled with inexact target: forget step passes, but re-introduction fails.17const baseInexact: {id: string, ...} = e; // OK18const widened: WithUnixTime = baseInexact; // ERRORincompatible-exactCannot assign baseInexact to widened because inexact object type [1] is incompatible with exact WithUnixTime [2].incompatible-typeCannot assign baseInexact to widened because property timestamp is missing in object type [1] but exists in WithUnixTime [2].
TypeScript는 클래스를 구조적으로 타입화합니다. interface, type, 그리고 클래스 인스턴스는 모양만 맞으면 대체로 서로 교환 가능합니다. Flow에서는 객체 타입 이 일반 객체(구체적으로는 객체 리터럴 {...}이 만들어내는 모양)를 설명하고, 인터페이스는 일반 객체, 클래스 인스턴스, 또는 인터페이스 타입 값이라면 무엇이든 만족할 수 있는 계약을 설명합니다. 여기에 클래스에 대한 Flow의 nominal typing까지 결합하면(같은 멤버를 가진 두 개의 다른 클래스는 서로 교환 가능하지 않음), 단방향 서브타이핑 규칙이 자연스럽게 따라옵니다.
타깃의 inexactness({a: number, ...})는 허용되는 일반 객체 속성의 폭만 넓히며, 받아들이는 값의 종류 까지 넓히지는 않습니다. 객체 타입은 오직 일반 객체만 설명합니다. 클래스 인스턴스와 인터페이스는 다른 종류이며, 그 자체로 항상 inexact합니다(인터페이스는 정의상 그렇고, 클래스 인스턴스는 서브클래스가 멤버를 추가할 수 있기 때문입니다). 그래서 inexact 객체 타입에 대해서도 여전히 실패하며, 단지 진단이 달라집니다.
[incompatible-exact]입니다.{a: number, ...})에 대해서는 오류가 [class-object-subtyping]이며, 메시지는 "Class instances are not subtypes of object types; consider rewriting object type as an interface."입니다.1// Flow:2class Foo {3 a: number = 1;4}5interface I { a: number }6type Obj = {a: number, ...};7
8declare function acceptsInterface(9 x: I,10): void;11declare function acceptsObj(12 x: Obj,13): void;14declare const someI: I;15
16acceptsInterface(new Foo()); // OK17acceptsInterface({a: 1}); // OK18acceptsObj({a: 1}); // OK19acceptsObj(new Foo()); // ERRORclass-object-subtypingCannot call acceptsObj with new Foo() bound to x because Foo [1] is not a subtype of Obj [2]. Class instances are not subtypes of object types; consider rewriting Obj [2] as an interface.20acceptsObj(someI); // ERROR: same [class-object-subtyping] codeclass-object-subtypingCannot call acceptsObj with someI bound to x because I [1] is not a subtype of Obj [2]. Class instances are not subtypes of object types; consider rewriting Obj [2] as an interface.
Flow에서 이런 문제를 만났을 때의 표준적인 해결책은 파라미터 타입을 객체 타입에서 인터페이스로 바꾸는 것입니다.
종류 구분에서 따라오는 추가 결과도 있습니다. 객체 타입은 this를 허용하지 않습니다(Flow는 객체 리터럴 안의 this를 [object-this-reference]로 거부합니다). 따라서 객체 타입은 this를 인식하는 동작이 아니라 순수한 데이터를 설명합니다. 이것이 implements/extends에 클래스나 인터페이스가 필요한 이유이고, 메서드 추출이 객체 타입에서는 안전하지만 클래스 인스턴스에서는 안전하지 않은 이유이기도 합니다.
한 가지 더 덧붙이면, TypeScript의 구조적 클래스 처리도 거의 전면적일 뿐 완전하지는 않습니다. const c: C = {x: 1}는 c가 클래스 인스턴스로 주석되어 있어도 TS에서 타입 검사를 통과합니다. TS는 구조적 기본 모델 위에 소수의 명목적 예외를 얹고 있습니다. ECMAScript #private 필드, private / protected 접근 수정자(둘 다 서로 다른 선언 간 할당 가능성을 차단), 그리고 unique symbol이 그것입니다. 하지만 이들은 전체적으로 구조적인 모델 위의 예외입니다. Flow의 클래스 nominalism은 전면적입니다. 클래스 정체성 자체가 명목 채널이므로, 별도의 명목 opt-in이 필요하지 않습니다. 이것이 Flow의 클래스/객체 오류가 TS 사용자에게 자주 놀라움을 주는 이유입니다.
implements와 extends 절의 오른쪽은 반드시 인터페이스나 클래스여야 합니다implements 또는 extends 절의 오른쪽은 TypeScript보다 Flow에서 더 엄격한 모양 규칙을 가집니다. TypeScript는 class C implements Omit<HTMLAttrs, "k">나 interface I extends Pick<Y, K>처럼 쓸 수 있으며, 어떤 객체 타입이든 동작합니다. Flow는 이런 절에서 객체 타입을 자체 진단과 함께 거부합니다.
class C implements ObjType는 [cannot-implement]와 함께 "Cannot implement ObjType because it is not an interface." 오류를 냅니다.interface I extends ObjType는 [incompatible-use]와 함께 "Cannot extend ObjType ... because ObjType is not inheritable." 오류를 냅니다.Flow에서의 표준적인 재작성은 인터페이스를 도입하는 것입니다(interface I { a: number; b: string } 후 class C implements I) 또는 멤버를 직접 인라인하는 것입니다. 인터페이스에 적용된 매핑/유틸리티 타입은 Flow에서도 동작하지만, 그 결과는 객체 타입이 되며, 바로 그 점 때문에 이런 절에서 받아들여지지 않습니다. 따라서 재작성은 객체 타입 별칭이 아니라 인터페이스 쪽으로 귀결되어야 합니다.
1// Flow:2type ObjType = {a: number, b: string};3class C implements ObjType { // ERROR: [cannot-implement]cannot-implementCannot implement ObjType because it is not an interface.4 a: number = 1;5 b: string = "hi";6}7interface I extends ObjType { // ERROR: [incompatible-use]incompatible-useCannot extend ObjType [1] with I because ObjType [1] is not inheritable.8 c: boolean;9}
TypeScript는 string / number / boolean을, 자신들이 만족하는 모든 인터페이스나 객체 타입에 구조적으로 할당 가능한 것으로 취급합니다. 원시값은 해당 boxed prototype(String.prototype / Number.prototype / Boolean.prototype)의 멤버와 대조되어 검사되므로, string은 String.prototype에 존재하는 멤버만 가진 어떤 인터페이스든 만족합니다(예: {length: number}, Iterable<string>). 이는 타입 수준에서의 호환성일 뿐이며, 런타임 boxing이 일어난다는 뜻은 아닙니다. Flow는 .js 파일에서 그런 검사를 수행하지 않습니다. 원시값이 인터페이스로 흐르면 [incompatible-type]("Cannot use string as a subtype of interface") 오류가 나고, 객체 타입으로 흐를 때도 일반적인 [incompatible-type] 코드로 오류가 납니다.
런타임 결과는 보통 로직 버그입니다. Iterable<string>은 Array<string>과 string 모두에 의해 만족됩니다. TS는 후자도 허용하므로, 호출자가 문자열 목록이 의도된 곳에 맨 문자열 하나를 조용히 넘길 수 있습니다. 그러면 for...of 루프는 항목이 아니라 문자들을 내놓게 됩니다.
// TypeScript:function logAll( items: Iterable<string>,) { for (const x of items) console.log(x);}logAll(["foo", "bar"]); // logs "foo" then "bar"logAll("fb"); // String.prototype[Symbol.iterator] yields characters, // so the loop walks code points: logs "f" then "b" instead of items
TypeScript는 wrapper promotion을 통해 이것을 허용합니다. String.prototype[Symbol.iterator]가 Iterable<string>을 만족하므로, string이 배열과 마찬가지로 흘러들어갑니다. Flow는 이를 거부합니다.
1// Flow:2function logAll(3 items: Iterable<string>,4) {5 for (const x of items) console.log(x);6}7const msgs = "foo";8logAll(msgs); // ERROR: stringis not a subtype ofIterable<string>incompatible-typeCannot call logAll with msgs bound to items because string literal foo [1], a primitive, cannot be used as a subtype of $Iterable [2]. You can wrap it in new String(...)) to turn it into an object and attempt to use it as a subtype of an interface.
Flow에서의 재작성은 호출 지점에서 이루어집니다. 한 원소짜리 리스트가 의도되었다면 문자열을 [s]로 감싸야 하고, 그렇지 않다면 호출 자체가 잘못된 모양입니다.
Flow는 {...A, b: T}를 타입 수준으로 끌어올립니다. type C = {...A, b: T}는 A의 속성과 b: T를 결합하는 실제 타입 주석입니다. TypeScript에는 타입 수준 spread가 없고, 대신 교차(type C = A & {b: T})를 사용합니다.
이것은 단순한 스타일 선택이 아닙니다. exact 객체 타입에서 자연스럽게 따라오는 결과입니다. Flow의 exact 객체 타입은 나열되지 않은 속성을 금지하므로, 두 개의 exact 객체 타입을 교차시키면 불가능한 타입이 생깁니다. 어떤 값이 정확히 A이면서 동시에 정확히 B여야 하는데, A와 B가 조금이라도 다르면 이는 만족 불가능하기 때문입니다(impossible intersection types 참고). 그래서 Flow에는 exact 객체 타입을 결합하는 다른 연산이 필요합니다. 타입 수준 spread({...A, ...B})가 바로 그 연산이며, 런타임의 값 수준 spread 의미를 직접 반영합니다. 즉, own 속성만 포함하고(인터페이스는 spread할 수 없음, 인터페이스는 own-vs-prototype을 추적하지 않기 때문), 나중 키가 앞선 키를 덮어쓰며, inexact 타입을 spread할 때는 결과도 inexact로 표시해야 합니다(named 속성보다 앞에 spread를 두고). 그렇지 않으면 소스에서 온 알 수 없는 속성을 exact 타깃에 건전하게 받아들일 수 없기 때문에 Flow가 spread를 거부합니다.
1// Flow:2type A = {x: number, y: string};3type C = {...A, z: boolean};4const c: C = {x: 1, y: "hi", z: true};
값 수준 spread에 대한 Flow의 타입화도 더 안전합니다. TypeScript는 {...c}를 마치 인터페이스의 모든 것을 복사하는 것처럼 타입화하지만, 값이 클래스를 기반으로 하고 있다면 프로토타입 메서드는 실제로 런타임 spread 결과에 존재하지 않으므로 이 타입화는 건전하지 않습니다.
// TypeScript:interface Counter { count: number; incr(): number;}class Impl { count = 0; incr(): number { return ++this.count; }}const c: Counter = new Impl();const o = {...c};o.incr(); // Runtime crash! 0incr0 is on 0Impl.prototype0, not on the spread result
Flow는 인터페이스가 own-vs-prototype을 추적하지 않기 때문에 결과 타입을 건전하게 만들 수 없어서 [cannot-spread-interface]로 거부합니다.
1// Flow:2interface Counter {3 count: number;4 incr(): number;5}6declare const c: Counter;7const o = {...c}; // ERROR: [cannot-spread-interface]cannot-spread-interfaceCannot spread object literal because Flow cannot determine a type for object literal [1]. Counter [2] cannot be spread because interfaces do not track the own-ness of their properties. Try using an object type instead.
this는 클래스 메서드에 바인딩되고 객체 리터럴에서는 금지됩니다Flow의 this 관련 두 규칙은 모두 같은 런타임 크래시를 미리 차단합니다. 메서드 본문이 this가 undefined인 상태로 실행되는 상황입니다. 클래스의 경우에는 추출 을 거부해서 호출이 리시버에 바인딩된 상태를 유지하게 하고, 객체 리터럴의 경우에는 그 구성 자체 를 거부해서 일반 객체 위에 추출 가능한 this 인식 메서드가 생기지 못하게 합니다.
this 사용 | 메서드 추출 | |
|---|---|---|
| 클래스 | 허용됨 | 금지됨 |
| 객체 리터럴 | 금지됨 | 허용됨 |
• 클래스 인스턴스에서 메서드 추출. TypeScript는 메서드를 일반 함수 값처럼 취급하고 조용히 추출을 허용합니다. 추출된 버전을 호출하면 메서드 본문은 this가 undefined인 상태로 실행되고, this.field를 통한 접근은 크래시를 일으킵니다.
// TypeScript:class Counter { count = 0; incr(): number { return ++this.count; }}const counter = new Counter();const tick = counter.incr;tick(); // Runtime crash! 0this0 is undefined, so 0++this.count0 throws
Flow는 클래스의 메서드 축약 속성에 대한 this 바인딩을 추적하기 때문에, [method-unbinding]("Cannot get counter.incr because property incr cannot be unbound from the context where it was defined.")으로 추출을 거부합니다.
1// Flow:2class Counter {3 count: number = 0;4 incr(): number {5 return ++this.count;6 }7}8const counter = new Counter();9const tick = counter.incr; // ERROR: [method-unbinding]method-unbindingCannot get counter.incr because property incr [1] cannot be unbound from the context [2] where it was defined.10const tickFixed = () =>11 counter.incr(); // OK - arrow captures this12tickFixed(); // OK
Flow에서의 재작성은 위의 tickFixed처럼 this를 포착하는 화살표 함수로 감싸는 것입니다.
• 객체 리터럴 내부의 this. TypeScript는 객체 리터럴 메서드 안의 this 참조를 허용합니다(TS는 this를 둘러싼 리터럴의 타입으로 추론합니다). 그러면 같은 추출 위험이 호출 지점에 적용됩니다.
// TypeScript:const counter = { count: 0, incr(): number { return ++this.count; }};const tick = counter.incr;tick(); // Runtime crash! 0this0 is undefined, so 0++this.count0 throws
Flow는 [object-this-reference]로 그 구성 자체를 금지하므로, 안전하지 않은 추출이 시도될 기회 자체가 사라집니다.
1// Flow:2const counter = {3 count: 0,4 incr(): number { return ++this.count; } // ERROR: [object-this-reference]object-this-referenceCannot reference this from within method incr [1]. For safety, Flow restricts access to this inside object methods since these methods may be unbound and rebound. Consider replacing the reference to this with the name of the object, or rewriting the object as a class.5};
Flow에서의 재작성은 this 대신 객체 리터럴 바인딩의 이름을 직접 사용하는 것입니다.
1// Flow:2const counter = {3 count: 0,4 incr(): number {5 return ++counter.count;6 } // Use object name directly instead of this7};8const tick = counter.incr; // Extraction allowed: no this in the function9tick(); // OK
일반 객체 타입 의 메서드 축약형({m(x: number): number})은 잃어버릴 this 문맥을 담고 있지 않으므로 추출이 괜찮습니다. 이것이 클래스 인스턴스는 객체 타입으로 흐를 수 없지만 객체 리터럴은 가능한 이유이기도 합니다.
선택 요소를 가진 튜플 타입을 다른 튜플 안으로 spread하는 것은 TypeScript에서는 허용되지만 부정확한 튜플 타입을 만들어냅니다. 런타임에서 선택 요소가 없으면 뒤쪽 위치들이 왼쪽으로 당겨지고, TS가 계산한 슬롯은 실제 런타임 레이아웃과 맞지 않게 됩니다.
// TypeScript:const middle: [middleName?: string] = [];const fullName: [ string, string | undefined, string,] = ["Ada", ...middle, "Lovelace"];fullName[2].toUpperCase(); // Runtime crash! 0fullName[2]0 is undefined, not "Lovelace"
TS 타입은 정적으로 알려져 있습니다(fullName은 [string, string | undefined, string]로 주석되어 있고 그 주석이 받아들여집니다). 하지만 건전하지 않습니다. 런타임에서 middle이 비어 있으면 위치 1의 값은 이동한 "Lovelace"가 되고 위치 2는 존재하지 않으므로, TS가 계산한 튜플은 런타임 레이아웃과 맞지 않습니다. Flow는 소스 튜플의 arity가 정적으로 고정되지 않았기 때문에 결과에 대해 건전한 정적 튜플 모양을 만들 수 없다고 보고, [invalid-tuple-arity]("array literal has an unknown number of elements")로 spread를 거부합니다.
1// Flow:2const middle: [middleName?: string] = [];3const fullName: [4 string,5 string | void,6 string,7] = ["Ada", ...middle, "Lovelace"]; // ERRORinvalid-tuple-arityCannot assign array literal to fullName because array literal [1] has an unknown number of elements, so is incompatible with tuple type [2].
Flow에서의 재작성은 선택 요소가 존재하는지 여부에 따라 명시적으로 분기하고, 각 분기에서 각각의 모양을 따로 조립하는 것입니다.
Flow의 분산성 기본값은 TypeScript보다 더 엄격합니다. 아래 하위 섹션들은 이를 선택하거나 해제하기 위한 키워드 문법과, 기본값이 갈라지는 위치들을 다룹니다.
| Surface | TypeScript | Flow |
|---|---|---|
| 분산성 키워드 | readonly 속성과 in / out 타입 파라미터를 사용하며, 명시적 불변성을 <in out T>로도 표기할 수 있습니다. | readonly / writeonly 속성과 in / out 타입 파라미터를 사용합니다. 기본 타입 파라미터 분산성은 invariant입니다. |
| 가변 객체 속성 | 공변. | 불변. |
readonly 속성의 할당 가능성 | readonly와 가변 속성은 읽기 전용 제약을 잃게 만들 수 있는 방식으로도 서로 할당 가능합니다. | 가변 속성 타입에 할당하는 식으로 readonly를 제거할 수 없습니다. |
| 가변 배열 | 공변. | 불변. |
| 제네릭 타입 인자 | 호환성 중심 예외를 포함해 사용법으로부터 분산성이 추론됩니다. | out 또는 in을 선언하지 않으면 기본적으로 invariant입니다. |
| 메서드 파라미터 | 메서드 문법에서는 이변적이고, 함수 타입 필드는 반공변입니다. | 반공변입니다. |
this 타입 위치 | 입력 위치와 invariant(가변 필드) 위치에서도 허용됩니다. | 공변 위치(반환 타입과 readonly 필드)로 제한됩니다. |
분산성 - 빠른 개요.
분산성 은 타입 T가 등장하는 위치를 통해 서브타이핑이 어떻게 흐르는지를 설명합니다. 예를 들어 {x: T}의 속성 타입, 함수 파라미터나 반환 타입, 또는 Container<T> 같은 제네릭 인자 위치입니다. Sub가 Super의 서브타입이라고 할 때, 그 위치는 다음 중 하나입니다.
{readonly x: Sub}는 {readonly x: Super}의 서브타입입니다. 읽기 전용 위치와 함수 반환 타입에 올바른 선택입니다.(x: Super) => void는 (x: Sub) => void의 서브타입입니다. 더 넓은 입력을 받는 피호출자는 더 좁은 입력을 주는 호출자를 만족시킵니다.{x: T}) 필요한 기본값입니다. 공변성은 쓰기를 깨고, 반공변성은 읽기를 깨기 때문입니다.Flow는 각 위치를 가장 엄격한 건전한 선택으로 기본 설정하고, TypeScript는 여러 위치에서 더 느슨한 기본값을 둡니다.
readonly / writeonly, in / out)Flow의 표준 분산성 문법은 TS와 정렬된 키워드 형태를 사용합니다. 속성에는 readonly / writeonly, 타입 파라미터에는 in / out을 씁니다. writeonly는 Flow 전용이라는 점에 유의하세요. TypeScript에는 write-only 대응물이 없습니다.
반대로, TypeScript의 결합형 <in out T>(명시적 불변성)는 Flow에 대응물이 없습니다. TypeScript는 사용법으로부터 분산성을 추론하고, 여러 호환성 중심 예외를 유지하기 때문에, 사용자가 원했던 더 엄격한 보장을 되찾기 위해 때로 다시 불변성을 선택해야 합니다. Flow의 기본값은 불변성이므로, 아무것도 쓰지 않았을 때 얻는 것이 바로 더 엄격한 선택입니다. 기본값의 대비에 대한 자세한 내용은 아래의 제네릭 타입 인자를 보세요.
표기 이상의 차이도 있습니다. Flow는 out T(또는 in T)로 선언한 타입 파라미터가 본문에서 선언된 분산성과 맞는 위치에서만 사용되는지 검증합니다. 입력 위치에서의 out T는 [incompatible-variance]와 함께 "Cannot use T in an input position because T is expected to occur only in output positions." 오류를 냅니다. TypeScript도 많은 위치에서 본문에 대한 in / out 검증을 수행합니다(예: interface Box<out T> { set: (t: T) => void }는 TS에서도 오류입니다. 함수 타입 필드 는 T를 반공변 위치에 놓기 때문입니다. 함수 입력은 분산성을 뒤집습니다). 더 좁은 차이는, TS가 out/in 주석이 붙어 있어도 메서드 축약형 을 계속 이변적으로 취급한다는 점입니다. 그래서 아래 Flow 형태(메서드 축약형으로 작성된)는 Flow에서는 오류지만 TS에서는 컴파일됩니다.
1// Flow:2type Box<out T> = {3 set(t: T): void; // ERROR: [incompatible-variance]incompatible-varianceCannot use T [1] in an input position because T [1] is expected to occur only in output positions.4};
이 하위 섹션은 문법 에 관한 것입니다. 각 위치에서 분산성이 실제로 어떻게 강제되는지라는 훨씬 더 중요한 의미적 차이에 대해서는 다음 하위 섹션을 보세요. 전체 동작 원리는 variance docs를 참고하세요.
아래 각 하위 섹션은 Flow가 더 엄격한 건전한 기본값을 선택하고, TypeScript가 더 느슨한 기본값을 선택하는 자리입니다. 이들을 합치면 타입 검사는 통과하지만 더 약한 정적 보장에 의존하는 TypeScript 코드의 큰 군집이 됩니다. 모든 예시는 런타임 예외를 던지거나 값을 조용히 손상시킬 수 있는 프로그램을 허용합니다.
{price: number}를 {price: number | string}에 할당하면 슬롯의 읽기 타입이 넓어질 뿐 아니라, 그 슬롯에 쓸 수 있는 값도 넓어집니다. 그 결과 이후의 p.price = "Free!"가 호출자 쪽의 number 타입 속성에 문자열을 써 넣게 됩니다.
// TypeScript:function markFree(p: { price: number | string,}) { p.price = "Free!";}const item: {price: number} = { price: 9.99,};markFree(item);item.price.toFixed(2); // Runtime crash! 0.toFixed0 is not a function on 0"Free!"0
TypeScript는 이 호출을 허용합니다. 속성이 공변이기 때문에 {price: number}는 {price: number | string}의 서브타입처럼 취급됩니다. 그 뒤 markFree 내부의 변이가 호출자 쪽의 number 타입 슬롯에 문자열을 씁니다. Flow는 이를 거부합니다.
1// Flow:2function markFree(p: {3 price: number | string,4}) {5 p.price = "Free!";6}7const item: {price: number} = {8 price: 9.99,9};10markFree(item); // ERROR: property price is invariantly typedincompatible-typeCannot call markFree with item bound to p because in property price: number [1] is not exactly the same as number | string [2].
해결책은 함수가 실제로 변이를 필요로 하는지에 달려 있습니다. 필요 없다면 파라미터를 읽기 전용으로 만드세요(Readonly<T> 유틸리티 또는 readonly 속성 수정자를 사용). p를 통한 변이 가능성이 사라지면 넓히기가 안전해집니다. 정말로 변이가 필요하다면(여기서는 markFree처럼), 호출자는 주석에 이미 더 넓은 타입이 포함된 소스를 제공해야 합니다. TypeScript도 같은 readonly 속성 수정자를 지원하지만, 그것을 강제하는 방식에서 두 언어는 다음 소절에서 갈라집니다.
1// Flow:2function logPrice(3 p: Readonly<{price: number | string}>,4) {}5const item: {price: number} = {6 price: 9.99,7};8logPrice(item); // OK
readonly 속성과 가변 속성이 상호 교환 가능하지만, Flow에서는 그렇지 않습니다{readonly value: T}를 {value: T}에 할당할 수 있다면, 호출자는 읽기 전용 제약을 버리고 기반 객체를 변경할 수 있게 됩니다.
// TypeScript:function f(obj: {value: number}) { obj.value = 99; // Silent mutation of a slot the caller annotated 0readonly0}const o: {readonly value: number} = { value: 1,};f(o);
TypeScript는 이 호출을 허용합니다. f를 통한 변이는 런타임에서 성공합니다. TypeScript는 직접적인 쓰기 지점에서는 readonly를 강제하지만, 할당 가능성은 readonly 제약을 떨어뜨릴 수 있습니다. Flow는 readonly / writeonly를 정적 안전성을 위한 실질적인 요소로 취급하고 이를 거부합니다.
1// Flow:2function f(obj: {value: number}) {3 obj.value = 99;4}5const o: {readonly value: number} = {6 value: 1,7};8f(o); // ERROR: [incompatible-variance]incompatible-varianceCannot call f with o bound to obj because property value is read-only in object type [1] but writable in object type [2].
해결책은 타깃도 읽기 전용으로 표시하는 것입니다({readonly value: number}). 이렇게 하면 f가 변이하지 않겠다고 선언하는 셈이므로, 제약을 떨어뜨리는 문제가 사라지고 호출이 성공합니다. f가 실제로 변이를 해야 한다면, 호출자는 대신 가변 소스를 제공해야 합니다.
1// Flow:2function f(3 obj: {readonly value: number},4) {}5const o: {readonly value: number} = {6 value: 1,7};8f(o); // OK
Array<string>을 Array<string | Error>처럼 취급하면, 피호출자 안의 .push(new Error(...))가 호출자의 문자열 타입 배열 안에 Error를 심을 수 있게 됩니다.
// TypeScript:function appendError( es: Array<string | Error>,) { es.push(new Error("oops"));}const errors: Array<string> = [];appendError(errors);errors[0].toUpperCase(); // Runtime crash! 0errors[0].toUpperCase0 is not a function
TypeScript는 이 호출을 허용합니다. push는 성공하고, 인덱스 0의 요소는 타입상 string이지만 런타임에서는 실제로 Error 인스턴스입니다. Flow는 이를 거부합니다.
1// Flow:2function appendError(3 es: Array<string | Error>,4) {5 es.push(new Error("oops"));6}7const errors: Array<string> = [];8appendError(errors); // ERROR: Array<string> is invariantly typedincompatible-typeCannot call appendError with errors bound to es because in array element: string [1] is not exactly the same as string | Error [2].
해결책은 피호출자가 변이를 해야 하는지에 달려 있습니다. 그렇지 않다면 파라미터를 ReadonlyArray<T>로 바꾸세요. es를 통한 변이 가능성을 제거하면 넓히기가 안전해집니다. ReadonlyArray가 Flow에 존재하는 이유는 바로 가변 형태가 invariant이기 때문 입니다. TS의 공변 패턴을 그대로 가져오다 놓치기 쉬운 점입니다. 피호출자가 실제로 변이를 해야 한다면(appendError처럼), 호출자는 요소 타입에 이미 더 넓은 선택지가 포함된 배열을 제공해야 합니다.
1// Flow:2function logErrors(3 es: ReadonlyArray<string | Error>,4) {}5const errors: Array<string> = [];6logErrors(errors); // OK
Flow는 제네릭 파라미터의 기본값을 invariant로 두고, 공변/반공변이 필요하면 out T / in T를 사용자가 선택하게 합니다. TypeScript는 사용법으로부터 분산성을 추론하고 호환성 중심 예외를 유지하므로, 읽기-쓰기 필드는 Flow의 기본값보다 더 약한 정적 보장을 갖는 경우가 있습니다.
// TypeScript:class Box<T> { value: T; constructor(value: T) { this.value = value; }}function widen( b: Box<number | string>,) { b.value = "oh no";}const box: Box<number> = new Box(1);widen(box);box.value.toFixed(); // Runtime crash! 0box.value0 is now "oh no", not a number
TypeScript는 이 호출을 허용합니다. widen은 호출자 쪽의 숫자 타입 슬롯에 문자열을 씁니다. Flow는 이를 거부합니다.
1// Flow:2class Box<T> {3 value: T;4 constructor(value: T) {5 this.value = value;6 }7}8function widen(9 b: Box<number | string>,10) {}11const box: Box<number> = new Box(1);12widen(box); // ERRORincompatible-typeCannot call widen with box bound to b because in type argument T [1]: string [2] is incompatible with number [3].
이런 모양의 Flow 제네릭을 작성할 때는 다음 중 하나입니다. 필드가 실제로 읽기 전용이라면 readonly와 out을 붙이세요. 그렇지 않다면 Flow의 invariant 기본값이 올바릅니다.
TypeScript의 메서드 파라미터 분산성은 이변적입니다. {compare(a: string, b: string): number}는 {compare(a: string | number, b: string | number): number}의 서브타입처럼 취급됩니다. 그 결과 더 넓은 타입은 숫자를, 문자열만 처리하는 본문에 넘길 수 있고, 런타임 크래시가 발생합니다. (TypeScript의 함수 타입 필드 는 반공변입니다. 이런 메서드-대-필드 비대칭 자체가 TS만의 특이점입니다.)
// TypeScript:type StringComparator = { compare(a: string, b: string): number,};type Comparator = { compare( a: string | number, b: string | number, ): number,};function callCompare(c: Comparator) { c.compare(1, 2); // Runtime crash! body calls .localeCompare on a number}const stringComparator: StringComparator = { compare(a, b) { return a.localeCompare(b); }};callCompare(stringComparator);
이 이변성 구멍은 호출 지점에서는 보이지 않습니다. TS 사용자는 런타임 전까지 오류를 보지 못합니다. Flow는 호출을 정적으로 거부하여 크래시가 발생하기 전에 버그를 잡습니다.
1// Flow:2type StringComparator = {3 compare(a: string, b: string): number,4};5type Comparator = {6 compare(7 a: string | number,8 b: string | number,9 ): number,10};11
12function callCompare(c: Comparator) {}13const stringComparator: StringComparator = {14 compare(a, b) {15 return a.localeCompare(b);16 }17};18callCompare(stringComparator); // ERRORincompatible-typeCannot call callCompare with stringComparator bound to c because: StringComparator [1] is incompatible with Comparator [2] in property compare > the first parameter: string [3] is incompatible with number [4]incompatible-typeCannot call callCompare with stringComparator bound to c because: StringComparator [1] is incompatible with Comparator [2] in property compare > the second parameter: string [3] is incompatible with number [4]
Flow의 구체적인 오류는 compare가 메서드 축약형인지 함수 타입 필드인지에 따라 달라집니다. 메서드 축약형(위 예시처럼)은 반공변성 때문에 실패합니다([incompatible-type]로 "the first parameter: string is incompatible with number"). 함수 입력은 분산성을 뒤집으므로, 그것을 넓히는 것은 건전하지 않습니다.
메서드 축약형을 가변 함수 필드로 바꾸면 검사가 느슨해지는 것이 아니라 더 엄격해집니다. 이제 속성 자체가 가변이므로, 오류는 불변성 이 됩니다(속성이 invariant하게 타입화됨). 이 경우 반대 방향의 (안전한) 할당까지도 막힙니다.
readonly compare를 붙이면 속성을 공변으로 만들어 그 안전한 방향(Comparator 타입 값이 StringComparator 슬롯으로 흐르는 경우)을 다시 허용할 수는 있습니다. 하지만 위의 예시는 여전히 해결되지 않습니다. 함수 입력의 반공변성이 넓은 입력을 막는 핵심이기 때문이며, 더 넓은 입력을 받게 하려면 처음부터 compare를 그 더 넓은 입력으로 선언해야 합니다.
전체 동작 원리는 variance docs와 subtyping docs를 참고하세요.
this 타입은 공변 위치로 제한됩니다this 타입은 fluent API와 다형적 메서드 리시버에 사용됩니다. 공변 위치(반환 타입, 그리고 Flow에서는 readonly 필드)에서는 두 언어 모두 이를 허용합니다. m(): this처럼 선언한 메서드는 fluent 체인을 따라 서브클래스 타입을 유지하므로, new SubBuilder().add(1).extra()는 SubBuilder 타입을 유지합니다. 차이는 입력 위치와 invariant(가변 필드) 위치에서 나타납니다. TypeScript도 그 위치들에서 this를 허용하지만, this 타입의 쓰기 가능한 슬롯은 호출자가 부모 클래스 인스턴스를 서브클래스 타입 필드에 써 넣을 수 있게 만듭니다. 그러면 이후 서브클래스 전용 메서드를 호출하는 접근이 크래시를 일으킵니다.
// TypeScript:class Builder { parent: this | null = null;}class SubBuilder extends Builder { extra(): this { return this; }}function stash(b: Builder) { b.parent = new Builder(); // 0b.parent0 is 0Builder | null0 here, so the write is allowed}const sb = new SubBuilder();stash(sb); // 0SubBuilder0 flows into 0Builder0; the write inside 0stash0 lands on 0sb.parent0if (sb.parent !== null) { // The null check only excludes null; it doesn't verify the class identity. // The unsound write in 0stash0 put a 0Builder0 in a slot typed 0this0 (= 0SubBuilder0). sb.parent.extra(); // Runtime crash! 0sb.parent0 is a 0Builder0, not a 0SubBuilder0}
Flow는 입력 위치와 invariant(가변 필드) 위치 모두에서 this를 [incompatible-variance]로 거부합니다. 이는 가변 객체 속성과 가변 배열을 invariant로 만드는 것과 같은 분산성 모델에서 자연스럽게 따라옵니다.
1// Flow:2class Builder {3 add(x: number): this { return this; } // OK: return type is covariant4 readonly origin: this | null = null; // OK: readonly field is covariant5 takesSelf(other: this): void {} // ERROR: input positionincompatible-varianceCannot use this [1] in an input position because this [1] is expected to occur only in output positions.6 parent: this | null = null; // ERROR: invariant fieldincompatible-varianceCannot use this [1] in an input/output position because this [1] is expected to occur only in output positions.7}
이 문제를 만났을 때의 재작성은 입력/필드 위치에서 클래스를 명시적으로 이름 짓는 것입니다(other: Builder, parent: Builder | null). 그 슬롯에서는 서브클래스 타입이 사라지는 것을 받아들여야 합니다. 또는 필드를 readonly로 만들어 그 위치를 공변으로 만들 수도 있습니다.
Flow에서 부재 또는 nullable 타입, 그리고 타입 계층의 top과 bottom을 어떻게 표기하는지에 대한 내용입니다. 이것들은 이름만 다른 차이입니다. 개념은 같고 표기만 다릅니다.
| 개념 | TypeScript | Flow | 참고 |
|---|---|---|---|
undefined만 거주하는 타입 | undefined | void | Flow에는 별도의 undefined 타입이 없습니다. (details) |
| "유용한 값이 없음" 반환 마커 | void | void | 이름은 같지만, Flow에는 오직 void만 있습니다(위 참고). |
| Nullable 값 (`T | null | undefined`) | `T |
| Bottom 타입 | never | empty | Flow가 empty를 기대하는 곳에서 TS 사용자는 자연스럽게 never를 떠올리게 됩니다. |
| Top 타입 | unknown | unknown | 이름이 같습니다. |
나머지 Flow 타입들과의 관계에서 이들이 어디에 놓이는지는 type hierarchy를 참고하세요.
void 대 undefinedFlow에서 주석으로서의 undefined는 즉시 오류입니다.
1// Flow:2function f(): undefined { // ERROR: [unsupported-syntax]unsupported-syntaxThe equivalent of TypeScript's undefined type in Flow is void. Flow does not have separate void and undefined types.3 return undefined;4}
1// Flow:2function f(): void {3 return undefined; // OK - 0undefined0 is the value inhabiting 0void04}
이 문제는 보통 TS 형태의 함수 시그니처를 Flow에 그대로 옮길 때 자주 나타납니다. 또한 Exclude<T, undefined>, T extends undefined ? ...처럼 제네릭 안에 undefined 리터럴 타입이 등장하는 TS 유틸리티 타입 코드에서도 나타납니다. Flow의 표준 형태는 다음과 같습니다. 주석에서는 undefined → void; null도 의도되었다면(대부분의 JS API) T | undefined → ?T; 부재만 의도되었다면 T | void.
관련해서 짚고 넘어갈 TS의 특이점도 있습니다. TypeScript에서는 () => undefined와 () => void의 할당 가능성이 비대칭입니다(undefined 반환은 void 슬롯을 만족하지만 그 반대는 아닙니다). Flow에는 void만 있으므로 이에 해당하는 것이 없습니다.
파라미터 타입에 void가 포함되면(T | void, ?T, 또는 T | null | void 어떤 표기든), 그 인자는 암묵적으로 선택 인자가 되어 호출자가 아예 생략할 수 있습니다. 이는 TypeScript와 다릅니다. TypeScript에서는 (x: T | undefined)라고 해도 호출 지점은 여전히 undefined를 전달해야 합니다.
1// Flow:2function f(x: ?number) {}3f(null); // OK4f(undefined); // OK5f(); // OK - ?Tincludesvoid, which makes the arg optional
Flow가 타입 가드의 본문을 어떻게 검증하는지, 어떤 중간 코드가 있으면 정제가 무효화되는지, 그리고 모듈 경계에서 어떤 검증(주석 요구사항과 값/타입 경계)을 수행하는지 설명합니다.
TypeScript는 x is T 프레디킷의 시그니처 를 검사합니다. 프레디킷 타입이 파라미터 타입에 할당 가능해야 하므로, function f(x: string): x is number 같은 선언은 거부됩니다. 하지만 TypeScript는 함수 본문이 주장한 정제를 실제로 구현하는지는 검사하지 않습니다. 본문은 신뢰되므로, 다음 코드는 typeof x === "object" && x !== null이 어떤 null이 아닌 객체({} 포함)에도 참인데도 TypeScript에서 타입 검사를 통과합니다. 이 가드에 의존하는 호출자는 속게 됩니다.
// TypeScript:type User = {id: string, name: string};function isUser(x: unknown): x is User { return typeof x === "object" && x !== null;}const data: unknown = {};if (isUser(data)) { data.name.toUpperCase(); // Runtime crash! 0data0 has no 0name0 property}
Flow는 타입 가드 함수의 본문을 양방향 모두에서 검증합니다. 각 방향은 고유한 진단으로 드러납니다.
• 긍정 방향 ([incompatible-type-guard]). 각 return 식에서, 정제된 파라미터 타입은 가드 타입의 서브타입이어야 합니다. 따라서 위 TypeScript 예제와 동등한 코드는 거부됩니다.
1// Flow:2type User = {id: string, name: string};3function isUser(x: unknown): x is User {4 return typeof x === "object" && x !== null; // ERROR: refined type isn't a subtype of Userincompatible-indexerCannot return ((typeof x) === "object") && (x !== null) because indexed object [1] is incompatible with exact User [2].incompatible-varianceCannot return ((typeof x) === "object") && (x !== null) because property id is read-only in object [1] but writable in User [2] and property name is read-only in object [1] but writable in User [2].incompatible-type-guardCannot return ((typeof x) === "object") && (x !== null) because in property id: unknown [1] is incompatible with string [2].incompatible-type-guardCannot return ((typeof x) === "object") && (x !== null) because in property name: unknown [1] is incompatible with string [2].5}
• 부정 방향 ([incompatible-type-guard]). 프레디킷이 false를 반환할 때, 그 부정은 파라미터에서 가드 타입을 완전히 제거해야 합니다. 그렇지 않으면 호출자는 제외되었어야 할 값을 보게 될 수 있습니다. x is A라고 타입이 붙어 있지만 실제로는 x instanceof B(엄격한 서브타입)를 검사하는 프레디킷은 이 이유로 거부됩니다.
1// Flow:2class A {}3class B extends A {}4function isA(x: unknown): x is A {5 return x instanceof B; // ERROR: negation does not refine A awayincompatible-type-guardCannot return x instanceof B because the negation of the predicate encoded in this expression needs to completely refine away the guard type A [1]. Consider using a one-sided type-guard (implies x is T).6}
진단은 명시적으로 탈출구를 제안합니다. "Consider using a one-sided type-guard (implies x is T)." 단방향 가드 (implies x is T)는 바로 이 부정 검사를 건너뜁니다. 함수가 true를 반환할 때만 파라미터를 T로 정제하고, false일 때는 그대로 두므로, 긍정 방향만 성립할 때 올바른 형태입니다.
전체 일관성 규칙은 type guards docs를 참고하고, implies 형태는 아래 단방향 타입 가드 섹션을 보세요.
Flow와 TypeScript는 모두 typeof, instanceof, 동등성, 타입 가드 등을 통해 타입을 좁히지만, 어떤 시점에 정제를 버리는지에 대한 규칙은 코드를 읽는 것만으로는 보이지 않는 방식으로 다릅니다. Flow는 중간 코드가 그 저장 위치의 실제 값을 바꿨을 수 있으면 정제를 무효화합니다.
x = ..., obj.k = ...).정제된 위치를 눈에 띄게 건드리지 않는 함수에 대한 단순한 호출은, 그 자체만으로는 지역 변수에 대한 정제를 떨어뜨리지 않습니다. 이것이 가장 흔한 과잉 수정입니다. TypeScript의 좁히기 또한(역시 간단하지 않은) 자체 무효화 모델을 가지며, 세부적으로는 Flow와 일치하지 않습니다. 같은 코드가 TS에서는 통과하고 Flow에서는 안 되거나, 그 반대일 수 있습니다. 전체 규칙은 refinement invalidations를 참고하세요.
1// Flow:2declare function sideEffect(): void;3
4function localCase(x: ?number) {5 if (x != null) {6 sideEffect(); // bare call does NOT drop the refinement on a local7 const a: number = x; // OK8 }9}10
11function propertyCase(obj: {12 x: ?number,13}) {14 if (obj.x != null) {15 sideEffect(); // bare call DROPS the refinement on a property16 const a: number = obj.x; // ERROR: callee could have mutated `obj.x`incompatible-typeCannot assign obj.x to a because null or undefined [1] is incompatible with number [2].17 }18}19
20function writeCase(x: ?number) {21 if (x != null) {22 x = null;23 const a: number = x; // ERROR: `x` is now typed `null` after the writeincompatible-typeCannot assign x to a because null [1] is incompatible with number [2].24 }25}
속성 사례의 표준 해결책은 정제된 값을 중간 코드 이전에 지역 변수로 꺼내는 것입니다. 일단 지역 변수가 되면 위의 단순 호출 예외가 적용되어 정제가 유지됩니다. write 사례는 정제된 바인딩에 재할당하지 않으면 됩니다. 새 값에는 별도의 지역 변수를 사용하세요.
1// Flow:2declare function sideEffect(): void;3
4function propertyCaseFixed(obj: {5 x: ?number,6}) {7 const {x} = obj;8 if (x != null) {9 sideEffect();10 const a: number = x; // OK - local refinement survives the call11 }12}
Flow는 함수 파라미터, export, 기타 주요 경계에 주석을 요구하며, 모듈의 export를 주석만으로 타입화할 수 없으면 [signature-verification-failure]를 보고합니다. export를 주석 없이 두고 타입 검사기가 모듈을 가로질러 추론하게 두는 데 익숙하다면, 그것은 Flow에서는 작동하지 않습니다. 주석이 반드시 있어야 합니다. TypeScript에도 opt-in 대응물(--isolatedDeclarations, TS 5.5+)이 있지만, strict의 일부가 아니며 기본적으로 꺼져 있습니다.
이것은 수백만 개 파일 규모의 저장소로 Flow가 확장될 수 있게 하는 의도적인 설계 선택입니다. 각 모듈의 export가 주석으로 완전히 기술되어 있기 때문에, Flow는 모듈 본문을 분석하지 않고도 그 모듈의 "타입화된 인터페이스"를 추출할 수 있고, 다른 모든 모듈을 그 인터페이스에 대해 병렬로 타입 검사할 수 있습니다.
전체 동작 원리는 annotation requirement docs와 그 안의 Module Exports 하위 섹션을 참고하세요.
// Flow:// ERROR: return type inferred, not annotated.export function getUser(id: string) { // [signature-verification-failure] return {id, name: 'Alice', age: 30};}// OK - annotate the return so the module's typed interface is self-contained.export function getUser(id: string): { id: string, name: string, age: number,} { return {id, name: 'Alice', age: 30};}
TypeScript의 import type와 export type은 기본적으로 지워지는 주석입니다. 실제 분류는 사용 지점에서 일어나므로, 타입 전용 export를 값 위치에서 import {Foo}로 가져와도 조용히 타입으로 해석됩니다. (--verbatimModuleSyntax에서는 import 지점에서 import type를 요구하고 그렇지 않으면 오류를 내지만, 이것은 opt-in 모드일 뿐 기본값이 아닙니다.) Flow는 import와 export 지점에서 값/타입 종류를 무조건 검증하며, 두 가지 별개의 진단을 냅니다.
export type만 한 모듈로부터 값 위치의 import {Foo}를 하면 [import-type-as-value]와 함께 "Cannot import the type Foo as a value. Use import type instead." 오류가 납니다.Foo가 타입 전용 바인딩인데 값 위치의 export {Foo}를 하면 [type-as-value]와 함께 "Cannot use type Foo as a value. Types are erased and don't exist at runtime." 오류가 납니다.두 경우 모두 해결책은 명시적인 타입 형태입니다. import type {Foo} 또는 export type {Foo}를 사용하세요. 이는 Flow의 시그니처 추출 모델에서 실질적인 역할을 합니다. 모듈이 노출하는 타입화된 인터페이스는 어떤 export가 타입이고 어떤 것이 값인지 모호하지 않아야 하기 때문입니다. 의존 모듈들은 본문 분석 없이 그 인터페이스만 보고 병렬로 검사됩니다.
// Flow:// mod.jsexport type Foo = {x: number};
// Flow:// consumer.jsimport {Foo} from './mod'; // ERROR: [import-type-as-value]import type { Foo as FooType,} from './mod'; // OK
TypeScript는 더 느슨한 표면 표기를 허용하지만 Flow는 명시적 형태를 요구하는 세 가지 위치가 있습니다. as 캐스트, 오류 suppression, 그리고 제네릭 인자 목록입니다.
as 캐스트는 Flow에서 더 안전합니다Flow의 as는 오직 넓히기 또는 단언만 합니다(예: 42 as number, 42 as 42). 안전하지 않은 다운캐스트는 타입 수준에서 거부합니다. {id: 1} as {id: number, name: string}는 Flow에서는 캐스트가 아니라 오류입니다. TypeScript의 as는 두 타입이 한쪽 방향으로라도 할당 가능하면 어떤 캐스트든 허용합니다. 그래서 안전하지 않은 다운캐스트도 조용히 승인할 수 있습니다. 아래 TypeScript 코드는 런타임에 존재하지 않는 name: string 속성을 캐스트가 만들어냈는데도 타입 검사를 통과하고, 이후 접근 시 런타임 크래시가 발생합니다.
// TypeScript:const u = {id: 1} as { id: number, name: string,};u.name.toUpperCase(); // Runtime crash! 0u.name0 is undefined
Flow는 넓히기와 단언 용도는 받아들이지만, 안전하지 않은 다운캐스트는 거부합니다.
1// Flow:2const n = 42 as number; // OK - widens the literal 0420 to 0number03const exact = 42 as 42; // OK - asserts at the literal type4const u = {id: 1} as {id: number, name: string}; // ERROR - unsafe downcastincompatible-typeCannot cast object literal to object type because property name is missing in object literal [1] but exists in object type [2].
강제 캐스트를 위한 Flow의 탈출구는 명시적인 두 단계 value as any as T입니다. TypeScript의 관용형은 value as unknown as T입니다.
Flow의 $FlowFixMe[code](및 $FlowExpectedError[code])는 해당 위치의 지정된 오류 코드만 suppression합니다. 같은 줄의 다른 오류는 여전히 나타나며, 대상 코드가 실제로 발생하지 않으면 suppression 자체가 사용되지 않은 suppression 경고로 보고됩니다. TypeScript의 // @ts-ignore는 다음 줄의 모든 오류를 무차별적으로 숨기고, // @ts-expect-error도 아무것도 suppression되지 않았을 때만 오류를 내면서 나머지는 모두 숨깁니다. Flow 형태가 훨씬 더 세밀하며 suppression 부채를 추적하기도 쉽습니다. 자세한 내용은 errors docs를 보세요.
1// Flow:2const o: {x: number} = {x: 1};3// $FlowFixMe[prop-missing] - intentional for demo4const y = o.nonexistent;
TypeScript는 모든 타입 파라미터에 기본값이 있으면 제네릭 타입을 인자 없이 쓸 수 있습니다. Foo<T = string> 다음에 type A = Foo라고 쓰면 A는 Foo<string>으로 해석됩니다. Flow는 이 bare 형태를 거부하고, 명시적인 타입 인자 목록(또는 기본값으로 돌아가기 위한 빈 <>)을 요구합니다. 오류 코드는 [missing-type-arg]이며 메시지는 "Cannot use Foo without 0-1 type arguments."입니다.
1// Flow:2type Foo<T = string> = {x: T};3type A = Foo; // ERROR: [missing-type-arg]missing-type-argCannot use Foo [1] without 0-1 type arguments.4type B = Foo<>; // OK - uses the default 0T = string0
재작성은 기계적입니다. 기본값만 있는 제네릭이면 Foo → Foo<>, 아니면 인자를 명시적으로 공급하면 됩니다. 명시적 형태를 요구하는 이유는 Flow가 bare 이름 Foo를 타입 생성자 자체 를 뜻하는 것으로 남겨두기 때문입니다. 그래서 타입에 대한 연산(예: 타입 수준 함수)이 적용되지 않은 형태를 입력으로 받을 수 있습니다. 이를 적용된 인스턴스의 축약형으로 과적재하지 않습니다.
이들은 Flow가 언어 또는 타입 검사기에 직접 내장한 기능들입니다. TypeScript는 해당 문제(React 컴포넌트 형태, hook 규칙, 렌더 제약, exhaustive 패턴 매칭, 모듈 경계를 넘는 nominal 추상화, 런타임이자 타입 수준인 enum)를 프레임워크/라이브러리 패턴, lint 규칙, 또는 사용자 코드에 맡겨 두는 경우가 많습니다. 즉, TypeScript에서 번역해 올 내장 대응물이 있는 것이 아니라, 새롭게 익혀야 할 Flow 개념들입니다.
component 문법Flow는 React 컴포넌트를 선언하기 위한 일급 component 문법을 제공합니다. 이에 해당하는 내장 TypeScript 대응물은 없습니다. TypeScript는 컴포넌트를 일반 함수로 모델링합니다. component 키워드가 일반 함수에 비해 제공하는 것은 다음과 같습니다.
({name}: {name: string}) 같은 구조 분해와 타입 표기의 중복을 없애고, props를 Readonly<{...}>로 감쌀 필요도 없게 합니다. 컴포넌트 파라미터는 기본적으로 읽기 전용입니다.React.Node를 추론하고 강제하며, 암묵적으로 반환하는 컴포넌트는 거부합니다.renders 절 로 컴포넌트가 생산할 수 있는 JSX 형태를 제한할 수 있습니다. 이는 아래 전용 섹션에서 다룹니다.this 없음, 중첩된 컴포넌트 정의 없음, 컴포넌트는 일반 함수 호출이 아니라 JSX로만 렌더링 가능.1// Flow:2import * as React from 'react';3
4component Greeting(5 name: string,6 greeting: string = "Hello",7) {8 return <p>{greeting}, {name}!</p>;9}10
11component App() {12 return <Greeting name="World" />;13}
TypeScript는 같은 모양을 일반 함수 컴포넌트와 props 타입으로 모델링합니다. Flow의 component와 달리, 이렇게 만들어진 값은 일반 함수로도 호출 가능합니다.
// TypeScript:import * as React from 'react';type GreetingProps = { name: string, greeting?: string,};function Greeting({ name, greeting = "Hello",}: GreetingProps) { return <p>{greeting}, {name}!</p>;}function App() { return <Greeting name="World" />;}const elem = Greeting({name: "World"}); // No error: TS treats components as callable
중첩된 컴포넌트 정의와 함수 스타일 호출은 각각 [nested-component]와 [react-rule-call-component]로 드러납니다.
1// Flow:2import * as React from 'react';3
4component Outer() {5 component Inner() { // ERROR: components may not be nested within other components or hooksnested-componentComponents may not be nested directly within other components or hooks.6 return <div />;7 }8 return <Inner />;9}10
11const x = Outer({}); // ERROR: components cannot be called; use JSX (<Outer />) insteadreact-rule-call-componentCannot call component Outer [1] because React components cannot be called. Use JSX instead. ()
hook 문법hook 문법은 React hook을 선언하기 위한 일급 Flow 키워드입니다. Flow는 이 키워드를 이용해 hook 호출 지점에서 Rules of React를 타입 수준에서 강제합니다. TypeScript에는 이에 해당하는 기능이 없습니다. TS에서 hook 규칙은 ESLint(eslint-plugin-react-hooks)가 강제하는데, 이는 타입 정보나 전체 프로그램 분석 없이 AST 패턴만 보고 동작합니다.
Flow는 함수 타입의 일부로서 hook 여부를 추적하므로, AST 기반 linter가 닿지 못하는 위반도 잡을 수 있습니다. 아래 예시에서 useToggle은 hook이 아닌 일반 함수가 기대되는 위치에 전달됩니다. Flow는 [react-rule-hook-incompatible] 오류를 냅니다. 호출 지점만 봐서는 fn이 피호출자 안에서 hook으로 호출될 것이라는 문법적 단서가 없기 때문에, ESLint의 AST 패턴 매칭은 이를 잡을 수 없습니다.
1// Flow:2import {useState} from 'react';3
4hook useToggle(5 initial: boolean,6): [boolean, () => void] {7 const [v, sv] = useState(initial);8 return [v, () => sv(x => !x)];9}10
11function callIt(12 fn: (boolean) => [boolean, () => void],13): [boolean, () => void] {14 return fn(false);15}16
17callIt(useToggle); // ERROR: `useToggle` is a hook; `fn` is notreact-rule-hook-incompatibleCannot call callIt with useToggle bound to fn because function [1] is a React hook but function type [2] is not a hook.
TypeScript에는 hook 여부라는 개념이 없습니다. useToggle은 그저 함수일 뿐입니다. 아래 호출은 타입 검사를 통과하지만, 런타임에서는 callIt이 컴포넌트나 hook 밖에서 useToggle을 호출하게 됩니다. eslint-plugin-react-hooks도 이것을 잡지 못합니다. 호출 지점에는 fn이 피호출자 안에서 hook으로 호출된다는 문법적 단서가 없기 때문입니다.
// TypeScript:import {useState} from 'react';function useToggle( initial: boolean,): [boolean, () => void] { const [v, sv] = useState(initial); return [v, () => sv(x => !x)];}function callIt( fn: (b: boolean) => [boolean, () => void],): [boolean, () => void] { return fn(false);}callIt(useToggle); // No type error; rule-of-hooks violation surfaces at runtime
renders 타입렌더 타입 (renders, renders?, renders*)은 컴포넌트의 합성 계약을 선언합니다. 어떤 슬롯이 무엇을 받아들이고, 어떤 컴포넌트가 무엇을 생산하는지를 나타냅니다. 디자인 시스템과 라이브러리는 래퍼 컴포넌트와 HOC를 가로질러 합성을 제한할 수 있고, 타입 검사기는 위반을 거부합니다. 내장된 TypeScript 대응물은 없습니다.
1// Flow:2import * as React from 'react';3
4component Header(5 text: string,6 color: string,7) {8 return (9 <div style={{color}}>{text}</div>10 );11}12component MainHeader(13 text: string,14) renders Header {15 return (16 <Header text={text} color="red" />17 );18}19
20component Layout(21 header: renders Header,22) {23 return (24 <div>25 {header}26 <section>Content</section>27 </div>28 );29}30
31const ok = (32 <Layout33 header={<MainHeader text="Flow" />}34 />35);36const bad = <Layout header={<footer />} />; // ERROR: `<footer />` does not satisfy `renders Header`incompatible-typeCannot create Layout element because in property header: React element [1] does not render Header [2].
TypeScript에는 이에 해당하는 기능이 없습니다. 가장 가까운 것은 슬롯을 React.ReactNode로 타입화하는 것인데, 그러면 어떤 노드든 받아들입니다. 즉, 합성 계약을 표현할 방법이 없기 때문에 아래에서 <footer /> 사례도 의도된 <MainHeader />와 마찬가지로 허용됩니다.
// TypeScript:import * as React from 'react';function Header({ text, color,}: {text: string, color: string}) { return ( <div style={{color}}>{text}</div> );}function MainHeader({text}: { text: string,}) { return ( <Header text={text} color="red" /> );}function Layout({header}: { header: React.ReactNode,}) { return ( <div> {header} <section>Content</section> </div> );}const ok = ( <Layout header={<MainHeader text="TS" />} />);const bad = ( <Layout header={<footer />} />); // No type error
match 표현식과 문장Flow에는 구조적 패턴, 가드, exhaustive 검사와 함께 쓰는 match 표현식과 문장이 있습니다. TypeScript에는 match가 없습니다. 문장 형태에 가장 가까운 대응물은 assertNever 폴스루를 가진 수작업 discriminated-union switch입니다. 표현식 형태에는 JavaScript의 switch가 문장 전용이기 때문에 직접적인 TS 대응물이 아예 없습니다. TS 사용자는 보통 중첩 삼항식이나 switch를 감싼 IIFE를 사용하지만, 그러면 match가 제공하는 구조적 패턴, 패턴 내부 변수 추출, exhaustive 검사를 모두 잃습니다.
1// Flow:2type Action =3 | {type: 'add', text: string}4 | {type: 'toggle', id: string}5 | {type: 'remove', id: string}6 | {type: 'filter', mode: 'all' | 'active' | 'done'};7
8declare const action: Action;9
10const description: string = match (action) {11 {type: 'add', const text} =>12 `Add: ${text}`,13 {type: 'toggle', const id} =>14 `Toggle ${id}`,15 {type: 'remove', const id} =>16 `Remove ${id}`,17 {type: 'filter', mode: 'all'} =>18 'Show all',19 {type: 'filter', mode: 'active'} =>20 'Show active',21 {type: 'filter', mode: 'done'} =>22 'Show done',23};
TypeScript에는 match가 없습니다. 중첩 삼항식이 표현식 형태에서 가장 가까운 대응물이지만, 구조적 패턴, 패턴 내 변수 추출, exhaustive 검사 모두 사라집니다.
// TypeScript:type Action = | {type: 'add', text: string} | {type: 'toggle', id: string} | {type: 'remove', id: string} | {type: 'filter', mode: 'all' | 'active' | 'done'};declare const action: Action;const description: string = action.type === 'add' ? Add: ${action.text}: action.type === 'toggle' ?Toggle ${action.id}: action.type === 'remove' ?Remove ${action.id} : action.type === 'filter' && action.mode === 'all' ? 'Show all' : action.type === 'filter' && action.mode === 'active' ? 'Show active' : action.type === 'filter' && action.mode === 'done' ? 'Show done' : (() => { throw new Error('unreachable'); })();
match는 exhaustive하게 검사됩니다. 어떤 케이스를 빼먹으면 [match-not-exhaustive]가 발생하고, 오류는 빠진 패턴의 이름을 알려줍니다. 새 action 타입이나 새 filter mode를 추가하면, 아직 그것을 처리하지 않은 모든 지점이 드러나게 됩니다.
1// Flow:2type Action =3 | {type: 'add', text: string}4 | {type: 'toggle', id: string}5 | {type: 'remove', id: string}6 | {type: 'filter', mode: 'all' | 'active' | 'done'};7
8declare const action: Action;9
10const description: string = match (action) { // ERROR: missing `{type: 'filter', mode: 'done'}`match-not-exhaustivematch hasn't checked all possible cases of the input type. To fix, add the missing pattern: {type: 'filter', mode: 'done'} to match object type [1].11 {type: 'add', const text} =>12 `Add: ${text}`,13 {type: 'toggle', const id} =>14 `Toggle ${id}`,15 {type: 'remove', const id} =>16 `Remove ${id}`,17 {type: 'filter', mode: 'all'} =>18 'Show all',19 {type: 'filter', mode: 'active'} =>20 'Show active',21};
Opaque 타입 별칭은 정의된 파일 밖에서는 그 내부 타입을 숨겨, 모듈 경계를 넘는 nominal 추상화를 강제합니다. TypeScript에는 이에 대한 네이티브 대응물이 없습니다. 그쪽에서 흔히 쓰는 관용구는 "브랜드 타입"으로, 보통 unique symbol 키를 가진 마커 속성과의 교차를 사용합니다. 이는 언어 기능이 아니라 사용자 코드 패턴입니다.
브랜드 타입 관용구가 강제하는 경계는 Flow의 파일 범위 추상화보다 약합니다. 브랜드 값은 as 캐스트 한 번이면 만들어낼 수 있습니다. 42 as UserId는 TypeScript에서 타입 검사를 통과합니다. 소스(number)와 타깃(number & {readonly [brand]: 'UserId'})이 number 위에서 겹치기 때문이며, TS는 양쪽이 완전히 분리되어 있을 때만 as 캐스트를 거부합니다.
반면 Flow의 opaque 타입은 모듈 경계 자체에 의해 봉인됩니다. 정의 파일 밖에서는 내부 타입이 전혀 보이지 않기 때문에, as 넓히기로 내부 표현으로부터 opaque 타입을 만들어낼 수 없습니다.
1// Flow:2opaque type UserId = number;3
4declare function makeUserId(5 n: number,6): UserId;7declare function lookupUser(8 id: UserId,9): string;10
11const id: UserId = makeUserId(42);12lookupUser(id); // OK13// In another file, `42` is not a `UserId` and a `UserId` is not a `number`.14// Inside this file (where the underlying type is visible) the conversion is allowed:15const n: number = id;
다른 파일에서 본 모습은 다음과 같습니다. 정의 모듈 밖에서는 내부 타입이 봉인됩니다. declare opaque type UserId(importer가 보게 되는 형태)는 그 봉인을 드러냅니다. 구조적으로 42를 만드는 것도, 다시 number로 투영하는 것도 둘 다 거부됩니다.
1// Flow (importer's view, as if `UserId` and functions were imported from another file):2declare opaque type UserId;3declare function makeUserId(4 n: number,5): UserId;6declare function lookupUser(7 id: UserId,8): string;9
10const id: UserId = makeUserId(42);11lookupUser(id); // OK12const forged: UserId = 42; // ERROR: number is not a UserIdincompatible-typeCannot assign 42 to forged because number [1] is incompatible with UserId [2].13const n: number = id; // ERROR: UserId is not a numberincompatible-typeCannot assign id to n because UserId [1] is incompatible with number [2].
맨 아래의 안전하지 않은 as 캐스트를 포함한 TypeScript 브랜드 타입 인코딩은 다음과 같습니다.
// TypeScript:declare const brand: unique symbol;type UserId = number & { readonly [brand]: 'UserId',};declare function makeUserId( n: number,): UserId;declare function lookupUser( id: UserId,): string;const id: UserId = makeUserId(42);lookupUser(id); // OK// 0as0 cast: source and target overlap on 0number0, so TS accepts.const forgedByCast = 42 as UserId;lookupUser(forgedByCast);
Flow Enum과 TypeScript enum은 겉보기에 비슷하지만 세부적으로는 매우 다릅니다.
| 측면 | TypeScript | Flow |
|---|---|---|
exhaustive switch | 내장 진단 없음. never나 lint로 인코딩해야 함. | 내장 지원. 멤버를 잊으면 [invalid-exhaustive-check]. |
| 내부 원시값으로의/으로부터의 암묵적 강제 변환 | 숫자 → 숫자 enum 슬롯을 허용하고(일치하지 않는 리터럴 제외), enum을 숫자로 자유롭게 강제 변환합니다. | 양방향 모두 차단됩니다. 안으로는 .cast()를 사용하고, 밖으로는 value as <representation type>(예: as string / as number)를 사용합니다. |
| 기본 멤버 값 | 숫자 enum은 0부터 자동 번호 매김. | 숫자 enum 멤버는 명시적으로 초기화해야 하며, 문자열 enum은 기본적으로 멤버 이름을 값으로 사용합니다. |
| 재선언 | 허용되며, 기본값과 조용히 충돌할 수 있습니다. | [name-already-bound]. |
| 역방향 매핑 | 숫자 enum은 런타임 역매핑을 가지며, 문자열 enum은 같은 접근에서 오류가 납니다. | .getName(value)가 숫자와 문자열 enum 모두에서 동작합니다. |
| 멤버 순회 | Object.values(Status)가 자연스러운 값 순회 형태지만, 숫자 enum에서는 런타임 역매핑의 값 그리고 멤버 이름 문자열이 모두 반환됩니다. | Status.members()는 값만 반환합니다. |
| symbol enum | 없음. | 지원됨 (enum X of symbol { ... }). |
| 정의 제한 | heterogeneous 초기화, 비리터럴 초기화, 소문자로 시작하는 멤버 이름을 허용합니다. | 세 가지 모두 오류입니다. |
이 중 몇 가지는 그 이유를 알아둘 가치가 있습니다.
StatusStr.Off는 키 "Off"가 아니라 값 "off"의 리터럴 타입을 가지므로, StatusStr[StatusStr.Off]는 존재하지 않는 StatusStr["off"]로 해석됩니다.Object.values는 [ 'Active', 'Paused', 'Off', 0, 1, 2 ]를 생성합니다. 런타임 역매핑의 양쪽 절반이 모두 결과에 들어갑니다(for...in도 같은 enum에서 키에 대해 같은 중복을 노출합니다)..cast, .members 같은 소문자 메서드를 노출하기 때문에 예약되어 있습니다.exhaustiveness는 내장되어 있습니다. Flow Enum에 대한 switch에서 어떤 케이스를 빼먹으면 [invalid-exhaustive-check]가 발생하며, 빠진 멤버 이름이 명시됩니다. 새 멤버를 추가하면 아직 그것을 처리하지 않은 모든 지점이 드러납니다.
1// Flow:2enum Status {3 Active,4 Paused,5 Off,6}7
8declare const st: Status;9
10let label: string;11switch (st) { // ERROR: member `Off` has not been consideredinvalid-exhaustive-checkIncomplete exhaustive check: the member Off of enum Status [1] has not been considered in check of st.12 case Status.Active:13 label = 'on';14 break;15 case Status.Paused:16 label = 'wait';17 break;18}
전체 동작 원리는 Flow Enums docs를 참고하세요.
implies)반환 타입이 implies param is T인 프레디킷 함수는, 함수가 true를 반환할 때만 파라미터를 T로 정제하고 false일 때는 그대로 둡니다. 이것은 위에서 다룬 본문 검증 규칙에서 긍정 방향만 성립할 때 사용하는 탈출구입니다. TypeScript에는 이에 해당하는 기능이 없습니다.
예를 들어 isPositive(n: ?number)는 양수에 대해서만 true를 반환합니다. 여기서 양방향 n is number는 건전하지 않습니다. false 분기는 null, void, 또는 0, -1 같은 음이 아닌이 아닌 수가 아니라 비양수 숫자일 수도 있는데, ?number에서 number를 빼면 null | void만 남기 때문입니다. implies 형태는 긍정 방향만 정제된다고 말하므로, else 분기는 원래의 ?number 타입을 유지합니다.
1// Flow:2function isPositive(3 n: ?number,4): implies n is number {5 return n != null && n > 0;6}7
8declare const n: ?number;9if (isPositive(n)) {10 n as number; // OK: refined to number11} else {12 n as ?number; // OK: stays ?number (not narrowed to null | void)13}
TypeScript에는 양방향 타입 가드만 있습니다. else 분기는 항상 프레디킷 타입을 제거하는 방향으로 정제되며, 그 정제가 건전하지 않아도 마찬가지입니다. 아래 코드는 타입 검사를 통과하지만, 추론된 else 분기 타입은 잘못되어 있습니다. 런타임에서 n은 실제로 비양수 숫자일 수 있습니다.
// TypeScript:function isPositive( n: number | null | undefined,): n is number { return n != null && n > 0;}declare const n: | number | null | undefined;if (isPositive(n)) { // n: number} else { // TS narrows n to null | undefined here, even though a non-positive // number (e.g. 0, -1) would also reach this branch at runtime.}
import typeofimport typeof는 Flow 전용 형태입니다. import type Foo from './m'는 Flow와 TypeScript가 공유하는 구문으로, 타입 export의 타입을 가져옵니다. 반면 import typeof Foo from './m'는 Flow 전용이며, 값 export의 타입을 가져와 타입 주석으로 사용할 수 있게 합니다.
TypeScript에서 import typeof에 가장 가까운 것은 typeof import('./m')이지만, 바인딩 형태가 다릅니다. TypeScript는 네임스페이스 형태를 만들고 보통 인덱스 접근과 결합해 사용합니다(typeof import('./m')['Foo']). 반면 import typeof Foo from './m'는 단일 값의 타입을 최상위 타입 바인딩으로 가져옵니다.
아래 예시는 두 형태를 모두 사용합니다. import type {Node}는 타입 export를 직접 타입 주석으로 가져오고, import typeof {useState}는 값 export의 타입을 가져옵니다. 제네릭 값의 타입은 import typeof를 거쳐도 여전히 타입 인자로 적용 가능합니다. 가져온 타입에 대해 useState<number>라고 쓸 수 있으므로, 가져온 함수 타입을 number 특수화로 인스턴스화하여 그 hook 파라미터를 number 타입의 useState로 호출할 수 있습니다.
1// Flow:2import type {3 Node as ReactNode,4} from 'react';5const node: ReactNode = "hello, world";6
7import typeof {useState} from 'react';8hook useCounter(9 useStateNum: useState<number>,10): number {11 const [count, setCount] =12 useStateNum(0);13 setCount(c => c + 1);14 return count;15}
몇몇 유틸리티 타입은 TypeScript 대응물이 없습니다. 가능한 경우 가장 가까운 TS 표기를 함께 적었습니다. 나머지는 네이티브 TS 형태가 없고 보통 사용자 코드 패턴으로 인코딩됩니다.
Class<T> - 인스턴스 타입 T에 대한 클래스 생성자 타입. TS의 네이티브 형태는 없으며, 보통 new (...args: any[]) => T 또는 특정 클래스라면 typeof T를 사용합니다.Values<T> - T 속성들의 값 타입 합집합. TS 표기는 인덱스 접근 T[keyof T]입니다.소수의 Flow 타입 주석 형태는 TypeScript 표기가 없습니다. tsc는 이를 파싱 단계에서 거부합니다. 이들은 기존 Flow 개념에 대한 대체 문법입니다.
interface 타입 주석 - type T = interface { foo: number }. 최상위 선언이 아니라 타입 식 안에 인터페이스를 놓을 수 있습니다. TypeScript는 별도의 interface I { ... } 문장을 요구합니다.Obj?.['prop']는 런타임의 ?. 연산자를 타입 수준에서 반영합니다. Obj가 nullish이면 결과는 void, 아니면 Obj['prop']입니다. TypeScript에는 타입 수준 ?.가 없습니다.type F = string => void. 파라미터 이름이 정보를 담지 않을 때 생략할 수 있습니다. TypeScript는 (x: string) => void를 요구합니다.type O = {[string]: number}. 같은 원리입니다. 인덱스 키 이름을 참조하지 않을 때 Flow는 생략할 수 있지만, TypeScript는 {[k: string]: number}를 요구합니다.1// Flow:2type Inline = interface { foo: number };3type Opt = ?{foo: number};4type Pulled = Opt?.['foo']; // number | void5type Fn = string => void;6type Dict = {[string]: number};
[options]에서 relay_integration=true를 설정하면, Flow는 graphql 태그드 템플릿 리터럴을 네이티브하게 이해하고 Relay 컴파일러가 생성한 아티팩트로부터 타입을 추론합니다. 그래서 사용자는 useFragment, usePreloadedQuery 등에 명시적 타입 파라미터를 생략할 수 있습니다. 관련 옵션으로는 relay_integration.esmodules(아티팩트를 CommonJS가 아니라 ES module default export로 해석), relay_integration.excludes(디렉터리별 opt-out)가 있습니다. 자세한 내용은 이 옵션 문서를 보세요.
TypeScript에는 타입 검사기 수준의 이에 해당하는 기능이 없습니다. TS 사용자는 생성된 타입을 명시적으로 전달하거나(useFragment<MyFragment$key>(...)), 편집기 힌트를 위한 TypeScript language service plugin 을 쓰거나(타입 검사 자체는 아님), 생성 타입을 명시적으로 import해야 하는 graphql-typed-document-node / gql.tada 같은 document-node 패턴을 사용합니다.
relay_integration=true일 때, Flow는 MyFragment에 대한 Relay 컴파일러 생성 아티팩트를 읽고 graphql 태그로부터 key와 result 타입을 추론합니다. 타입 파라미터도, 생성 타입 import도 필요 없습니다.
// Flow:import { graphql, useFragment,} from 'react-relay';declare const userRef: MyFragment$key;const data = useFragment( graphqlfragment MyFragment on User { name }, userRef,);
TypeScript는 명시적인 타입 파라미터와 생성 타입 import를 요구합니다.
// TypeScript:import { graphql, useFragment,} from 'react-relay';import type { MyFragment$key,} from './__generated__/MyFragment.graphql';declare const userRef: MyFragment$key;const data = useFragment<MyFragment$key>( graphqlfragment MyFragment on User { name }, userRef,);
이들은 현재 Flow에 대응물이 없는 TypeScript 기능들입니다. 일부는 Flow가 의도적으로 채택하지 않았습니다. 대개는 Flow 기능과 겹치지만 다른(보통 더 보수적인) 기본값을 가지거나, Flow 설계가 피하려는 위험 요소를 들여오기 때문입니다. 다른 일부는 아직 단순히 구현되지 않았습니다(진행 중이거나 릴리스 게이트 대기 중인 기능은 별도의 Coming soon 섹션을 보세요). 아래 항목들을 Flow 코드에서 사용하려 하면 동작하지 않으며, 경우에 따라 TypeScript 문법은 파싱은 되기 때문에 실패가 예상보다 늦게 드러날 수도 있습니다.
소수의 TS 표면 문법 형태는 Flow 표기가 없지만, 개념 자체 는 Flow에 다른 이름으로 존재합니다. Flow는 TS 형태를 파싱/타입 검사 단계에서 거부하며, 진단은 바로 Flow 재작성 형태를 가리킵니다.
<T>x → Flow x as T.[number, string?] → Flow [a: number, b?: string]. Flow는 선택 요소에 대해 라벨이 붙은 변형을 요구합니다.readonly 타입 연산자 - TS readonly [T, S] → Flow Readonly<[T, S]>.readonly 타입 연산자 - TS readonly T[] → Flow ReadonlyArray<T>.참고로 속성 수정자로서의 readonly({readonly x: T})와 타입 파라미터의 out T는 두 언어에서 동일하게 동작합니다. 자세한 내용은 분산성 키워드를 보세요. 위의 두 readonly 형태는 구조 타입 앞에 붙는 타입 연산자 로서의 readonly 사용이며, 이 표기는 TS 전용입니다. Flow는 대신 래퍼 유틸리티를 사용합니다.
Flow는 데코레이터 문법을 파싱하지만 타입 검사는 하지 않습니다. 데코레이터의 타입은 기반 값에 전혀 적용되지 않으며, decoration은 조용히 지워집니다. TypeScript는 서로 호환되지 않는 두 모드를 지원합니다. 기본값인 TC39 decorators(컨텍스트 객체 파라미터 사용)와, --experimentalDecorators에서의 legacy decorators(옛 (target, key) 시그니처)입니다.
TypeScript는 여러 클래스 문법 확장을 가지지만, Flow는 이를 의도적으로 채택하지 않고 동등한 JS를 직접 작성하라고 요구합니다.
파라미터 프로퍼티 (constructor(public x: number)) - 런타임 코드를 생성하는 TS 전용 축약형입니다. 필드를 자동 선언하고 생성자 인자로부터 할당합니다. Flow의 진단은 다음과 같습니다. "Flow does not support TypeScript parameter properties. To fix, declare the property in the class body and assign it in the constructor."
public / protected / private 접근 수정자 - TS가 검사하는 접근 제어입니다. 이것들은 TypeScript에서 타입 검사기 전용이며(필드는 런타임에서 여전히 공개 접근 가능), 제거해도 안전합니다. Flow는 셋 다 거부합니다. 그냥 제거하거나(public / protected는 런타임 효과가 없음), private foo를 #foo로 옮기세요. #private 재작성은 TS 형태와 런타임 모양이 달라집니다. ECMAScript #private 필드는 런타임에서 명목적으로 비공개이지만, TS의 private는 지워집니다.
accessor auto-accessors (class C { accessor x: T = init }) - private 필드를 기반으로 getter/setter 쌍으로 디슈가되는 TC39 제안입니다. Flow는 이 형태를 파싱하지 않습니다. getter와 setter를 #private 백킹 필드와 함께 명시적으로 작성하거나, accessor 래핑이 필요 없다면 일반 필드를 사용하세요.
namespace 블록소스 수준의 namespace { ... } 블록은 없습니다. Flow에는 libdef 안에서 ambient 선언을 위한 declare namespace는 있지만, 런타임 값을 생성하는 소스 수준 namespace 블록은 없습니다.
const enum대응물이 없습니다. Flow의 변환기는 const enum처럼 사용 지점에 인라인하지 않고, Flow Enum을 런타임 객체로 출력합니다. 그래도 그 제한들(리터럴 값만 허용, 재선언 없음, 기본 숫자값 없음)은 빌드 시스템 차원의 인라이닝을 원할 경우 충분히 단순하게 만들어 줍니다.
TypeScript의 asserts x is T 반환 타입은, assertion이 실패하면 예외를 던지고 호출이 정상 반환된 뒤에는 파라미터를 무조건 T로 정제하는 함수를 선언합니다. 이는 boolean을 반환하고 if/else 안에서만 정제하는 타입 가드와는 다른 형태입니다. Flow에는 타입 가드(x is T)는 있지만 asserts x is T 형태는 없습니다.
가장 가까운 Flow 대응물은 타입 가드와 호출 지점의 명시적 throw를 결합하는 것입니다. 예: function isStr(x: unknown): x is string { ... } 다음에 if (!isStr(x)) throw new Error();.
ThisType<T> 유틸리티TypeScript의 ThisType<T>는 문맥 타입 안에서 객체 리터럴 메서드의 this를 T로 다시 배선하는 마커입니다. Flow는 그런 재배선을 구현하지 않습니다. ThisType<T>가 TypeScript에서 유용한 이유가 되는 두 언어 차이는 Flow에는 없습니다.
this 참조를 아예 거부하므로, Flow가 this를 다시 배선할 객체 리터럴 메서드 본문 자체가 없습니다.this 바인딩을 가지며, 외부 마커로 재할당할 수 없습니다.TypeScript는 값 표현식 에 타입 인자를 붙이는 것을 허용합니다. Foo<string>를 독립 표현식으로 써서 제네릭을 특수화하고 이름에 바인딩할 수 있습니다. Flow는 이 형태를 파싱하지 않으며 닫는 > 직후에 ParseError를 냅니다.
// TypeScript:declare class Foo<T> { value: T;}const StringFoo = Foo<string>;
Flow에서의 재작성은 미리 특수화된 바인딩에 이름을 붙이는 대신, 인스턴스화나 호출 지점에서 타입 인자를 공급하는 것입니다(new Foo<string>(), f<string>(x)).
TypeScript는 discriminated union의 구조 분해된 속성들을 함께 좁힙니다. sentinel 바인딩을 정제하면, 같은 구조 분해에서 추출된 다른 바인딩들도 함께 정제됩니다.
// TypeScript:type FormField = | {kind: 'text', value: string} | {kind: 'number', value: number};declare const field: FormField;const {kind, value} = field;if (kind === 'text') { const s: string = value; // OK - TS narrows 0value0 based on 0kind0} else { const n: number = value;}
Flow는 sentinel 태그가 붙은 union을 원래 값 을 통해 정제합니다(if (field.kind === 'text') { ... field.value ... }). 하지만 각 구조 분해 바인딩은 서로 독립적으로 전체 union 타입을 유지하므로, 같은 코드는 실패합니다.
1// Flow:2type FormField =3 | {kind: 'text', value: string}4 | {kind: 'number', value: number};5
6declare const field: FormField;7const {kind, value} = field;8if (kind === 'text') {9 const s: string = value; // ERROR: `value` keeps its full `string | number` typeincompatible-typeCannot assign value to s because number [1] is incompatible with string [2].10}
Flow에서의 재작성은 구조 분해하지 말고 원래 값을 통해 정제하는 것입니다. 또는 match 표현식이나 문장을 사용하면 매칭과 구조 분해를 한 형태 안에서 처리할 수 있습니다. 각 arm은 sentinel을 매치하고, value를 arm별 타입으로 바인딩합니다.
1// Flow:2type FormField =3 | {kind: 'text', value: string}4 | {kind: 'number', value: number};5
6declare const field: FormField;7match (field) {8 {kind: 'text', const value} => {9 const s: string = value;10 }11 {kind: 'number', const value} => {12 const n: number = value;13 }14}
소스 수준 대응물이 없습니다. TypeScript 사용자는 declare module 'name' { ... }를 소스 코드에서 사용해 서드파티 모듈을 다시 열고 타입을 추가하는 일을 자주 합니다. Flow의 declare module은 임의의 소스 파일이 아니라 flow-typed/ 아래의 libdef 안에서만 사용합니다.
다음 TypeScript 기능들은 Flow에서 타입 검사 지원 구현이 이미 되어 있지만, Flow가 릴리스 게이트를 제거하기 전에 도구 업데이트(예: Prettier)를 기다리고 있습니다. Flow playground에서는 활성화되어 있습니다.
${'a' | 'b'}-${'x' | 'y'}.-readonly와 as 키 리매핑.override.type Ctor = new (x: number) => R.satisfies 표현식 - 추론된 타입을 넓히지 않고 식이 어떤 타입을 만족하는지 검증합니다.import() 타입 표현식 - type A = import('./m').A.import X = require('foo')와 export = X - CommonJS 스타일 import/export 바인딩.계획 중:
unique symbol 문법은 현재도 파싱되지만, 타입 시스템은 아직 symbol 키를 서로 다른 nominal 키로 모델링하지 않습니다.이 표는 기존 Flow 형태를 현대적인 Flow 대체 형태에 매핑합니다. 어떤 행은 단순한 문법 이름 바꾸기이고, 다른 것은 오래된 유틸리티나 TS 정렬 대응물이 있는 기능입니다. 새 코드에서는 오른쪽 형태를 사용해야 합니다.
| Legacy Flow | Modern Flow (TS-aligned) |
|---|---|
mixed | unknown |
$Keys<T> | keyof T |
$ReadOnly<T> | Readonly<T> |
$NonMaybeType<T> | NonNullable<T> |
$ReadOnlyArray<T> | ReadonlyArray<T> |
<T: Bound> | <T extends Bound> |
(x: T) cast | x as T |
| `{ | a: number |
+foo / -foo property variance | readonly foo / writeonly foo (writeonly is Flow-specific) |
+T / -T type parameter variance | out T / in T |
%checks predicate functions | 사용자 정의 type guards (function isString(x: unknown): x is string) |
$ObjMap<O, F> / $ObjMapi<O, F> / $TupleMap<T, F> / $TupleMapi<T, F> | 함수 본문을 인라인한 mapped types ({[K in keyof O]: ...}) |
$PropertyType<T, K> / $ElementType<T, K> | indexed access (T[K]) |
$Call<F, ...Args> | ReturnType<F>와 인덱스 접근, 또는 infer를 사용한 conditional type |
$Diff<A, B> / $Rest<A, B> | 대개 Omit<A, keyof B>. 경우에 따라 다르며(항상 의미적으로 동일하지는 않음) |
전체 그림은 Modernizing Legacy Flow Syntax를 참고하세요.
몇몇 Flow .flowconfig[options] 토글은 TypeScript compilerOptions의 엄격성 플래그와 직접 대응합니다. 의미는 같지만 기본값은 다릅니다. 이 페이지에서 사용한 TypeScript strict 기준에서는 useUnknownInCatchVariables가 strict를 통해 활성화되지만, noUncheckedIndexedAccess는 strict의 일부가 아니어서 opt-in 상태로 남습니다. Flow에는 strict 옵션 묶음이 없습니다. 두 플래그 모두 개별 opt-in이고 기본값은 false입니다. 따라서 strict가 켜진 TS 프로젝트에서 옮겨올 때는 맞추기 위해 use_unknown_in_catch_variables를 켜야 합니다.
| TypeScript option | Flow option | Description |
|---|---|---|
noUncheckedIndexedAccess | no_unchecked_indexed_access | 배열이나 딕셔너리를 통한 인덱스 접근은 결과 타입에 undefined(Flow에서는 void)를 추가합니다. 그래서 arr[i]나 dict[k]를 읽으면 T가 아니라 `T |
useUnknownInCatchVariables | use_unknown_in_catch_variables | 주석이 없는 catch 바인딩의 기본 타입을 any에서 unknown으로 바꿉니다. 사용자는 값을 사용하기 전에 (instanceof Error, typeof e === 'string', …) 좁혀야 합니다. 자세한 내용은 docs를 참고하세요. |
TypeScript의 .d.ts 파일은 서로 다른 두 가지 관심사를 함께 다룹니다. 서드파티 npm 패키지의 타입 지정과, 일반 JavaScript로 유지되어야 하는 자사 코드(또는 vendored code)의 타입 지정입니다. Flow는 이를 두 가지 메커니즘으로 분리합니다.
| TypeScript 사용 사례 | Flow 메커니즘 | 배치와 해석 |
|---|---|---|
@types/* 패키지와 패키지 수준 .d.ts 파일을 포함한 서드파티 패키지 선언. | 라이브러리 정의 (libdef). | 보통 flow-typed/ 안의 일반 .js 파일이며, 대개 declare module 'pkg' { ... } 형태로 패키지 이름을 지정합니다. |
| JavaScript 구현 파일 옆의 형제 선언 파일. | 선언 파일. | 구현 파일을 가리는 함께 놓인 .js.flow 또는 .json.flow 파일(예: Misc.js 옆의 Misc.js.flow). |
임의 프로젝트 파일에서 declare module 'pkg' { ... }를 사용하는 소스 수준 모듈 확장. | 소스 수준 대응물 없음. | Flow의 declare module 'pkg' { ... } 형태는 일반 소스 파일이나 함께 놓인 .js.flow 선언 파일에서 모듈을 다시 여는 용도가 아니라, flow-typed/ 아래의 libdef용입니다. |
이 두 Flow 메커니즘은 TypeScript의 ambient 선언 문법 상당 부분을 공유하지만, 배치 위치가 실질적으로 중요합니다. declare class, declare function, declare const 등은 libdef, 선언 파일, 또는 인라인 선언에서 ambient 값을 기술할 수 있습니다. declare module 'name' { ... }는 libdef가 사용하는 이름 있는 패키지 형태입니다. 선언 파일은 보통 함께 놓인 모듈의 export를 직접 기술합니다. 예를 들어 declare export ... 또는 declare module.exports를 사용합니다.
TypeScript가 지원하지만 Flow는 지원하지 않는 선언 파일 패턴은 사용자 측 모듈 확장을 보세요.
Flow는 TypeScript와 같은 분리된 네임스페이스 모델을 사용하며, declaration merging의 일부를 지원합니다. 각 이름은 값 네임스페이스와 타입 네임스페이스에 독립적으로 존재하므로, 하나의 식별자가 값이면서 타입이어도 충돌하지 않습니다. const A = 1; interface A {}는 허용되며, 값 위치의 A는 const로, 타입 위치의 A는 인터페이스로 해석됩니다. 양쪽 네임스페이스에서 모두 사용 가능한 구성요소(클래스, enum)는 값 네임스페이스에 한 번만 등록되고, 타입 쪽은 거기로 폴백합니다.
Flow가 병합하는 것:
interface + interface - 멤버들의 합집합(호환되는 중복은 허용, 충돌하는 멤버는 오류). 라이브러리 정의 파일에서는 extends 목록도 이어 붙여지고, 호출 시그니처는 교차로 오버로드되며, 타입 파라미터 arity 불일치는 오류가 납니다. 일반 소스 파일에서는 병합이 멤버에 한정됩니다. extends 목록과 호출 시그니처는 선언 간에 결합되지 않으며, 같은 이름의 인터페이스 둘을 같은 파일에서 모두 export할 수도 없습니다.declare class + interface - 인터페이스 멤버가 클래스에 합쳐집니다(순서 무관).function / declare function + declare namespace - namespace의 타입 멤버가 함수에 합쳐집니다(순서 무관). fn.T처럼 접근합니다.class / declare class + declare namespace - namespace의 타입 멤버가 클래스에 합쳐집니다(순서 무관). Cls.T처럼 접근합니다.Flow가 하지 않는 것:
declare namespace의 타입 멤버만이 형제 함수나 클래스에 안정적으로 전파됩니다. 값 멤버는 호스트의 런타임 속성으로 취급되지 않습니다. namespace가 런타임 멤버를 기여하는 TS 스타일 function + namespace 값 측 병합은 지원되지 않습니다.declare module 병합. 같은 모듈에 대한 여러 declare module 'name' { ... } 블록은 합쳐지지 않습니다. 두 번째 것은 첫 번째 것의 override로 취급됩니다. 어떤 모듈에 대한 libdef는 한 곳에 있어야 합니다.declare module 'name' { ... } 소스 수준 확장(사용자 측 모듈 확장 참고).위 메커니즘들은 선언을 입력 으로 사용하는 경우, 즉 타입 검사기가 볼 수 없는 코드를 타입화하는 방법에 관한 것입니다. 반대 방향은 소스로부터 선언 파일을 생성 하는 일입니다. TypeScript는 이것을 컴파일러 자체에서 처리합니다. tsc --declaration은 컴파일된 각 .ts 옆에 .d.ts를 생성하고, --emitDeclarationOnly는 대응하는 .js 없이 선언만 생성합니다. Flow에는 flow 바이너리에 내장된 대응물이 없습니다. 별도의 flow-api-translator NPM 패키지가 이 간극을 메우며, Flow 소스 파일로부터 .js.flow 또는 .d.ts 파일을 생성합니다.
$ 접두 유틸리티와 다른 오래된 문법 형태를 현대적인(종종 TS 정렬된) 대응물로 마이그레이션하는 전체 참고서입니다.