Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 827 – 타입 조작

Author:
Michael J. Sullivan <sully at vercel.com>, Daniel W. Park <daniel.park at vercel.com>, Yury Selivanov <yury at vercel.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Typing
Created:
27-Feb-2026
Python-Version:
3.16
Post-History:
02-Mar-2026

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

Python 타입 시스템에 강력한 타입 수준의 introspection 및 구성 기능을 추가할 것을 제안합니다. 이 설계는 주로 TypeScript의 조건부 타입과 매핑된 타입에서 영감을 받았지만, Python 타이핑 모델의 고유한 의미론과 제약에 맞게 조정되었습니다.

큰 틀에서 보면, 이 제안은 다음을 목표로 합니다:

  • typing 모듈에 새로운 기본 요소를 도입합니다;
  • Python 문법을 변경하지 않고 정적 타입 검사기가 지원하는 어노테이션 문법을 확장합니다;
  • 새로운 타입 수준 기능이 정적 타입 검사기와 런타임 타입 introspection에 의존하는 프레임워크 모두에 이점을 제공하도록 보장합니다.

동기

Python에는 점진적 타입 시스템이 있지만, 그 핵심에는 상당히 전통적인 정적 타입 시스템이 있습니다.

반면 Python이라는 언어에서는 특히 라이브러리와 프레임워크에서 복잡한 메타프로그래밍을 수행하는 일이 드물지 않습니다. 타입 시스템은 일반적으로 메타프로그래밍을 모델링할 수 없습니다.

메타프로그래밍과 타입 시스템 사이의 간극을 메우기 위해 일부 라이브러리는 사용자 정의 mypy 플러그인을 함께 제공합니다. 데이터클래스와 유사한 변환 사례는 특수 사례를 추가할 만큼 충분히 일반적이라고 여겨졌으며, 해당 사례를 특별히 다루기 위해 @dataclass_transform 데코레이터가 추가되었습니다 (PEP 681). 이 접근 방식의 문제는 많은 타입 검사기에 플러그인 API가 없고 앞으로도 없을 것이므로 IDE, CI 및 도구 전반에서 일관된 타입 검사를 달성할 수 없다는 점입니다.

Python 언어와 그 타입 시스템 사이의 표현력 차이가 상당하다는 점을 고려하여, 동적인 Python 코드를 더 잘 따라갈 수 있는 타입 조작 기능을 추가함으로써 이 간극을 메울 것을 제안합니다.

이에 대한 수요가 있습니다. Meta의 2025 Typed Python Survey 응답을 분석한 결과, “가장 많이 요청된 기능” 목록의 첫 번째 항목은 다음과 같았습니다:

TypeScript 및 기타 언어에 없는 기능: 많은 응답자가 교집합 타입(& 연산자와 같은), 매핑된 타입 및 조건부 타입, 유틸리티 타입(Pick, Omit, keyof 및 typeof와 같은), 그리고 딕셔너리/dict를 위한 더 나은 구조적 타이핑(예: 더 유연한 TypedDict 또는 익명 타입) 등 TypeScript에서 영감을 받은 기능을 요청했습니다.

더 강력한 타입 조작으로 해결할 수 있는 문제의 몇 가지 예를 제시하겠지만, 이 제안은 일반적이며 훨씬 더 많은 사용 사례를 가능하게 할 것입니다.

Prisma 스타일 ORM

TypeScript에서 널리 사용되는 ORM인 Prisma는 다음과 같이 TypeScript로 데이터베이스 쿼리를 작성할 수 있게 합니다 (this example에서 조정함):

const user = await prisma.user.findMany({
  select: {
    name: true,
    email: true,
    posts: true,
  },
});

이때 user의 추론된 타입은 다음과 같은 형태가 됩니다:

{
    email: string;
    name: string | null;
    posts: {
        id: number;
        title: string;
        content: string | null;
        authorId: number | null;
    }[];
}[]

여기서 출력 타입은 prisma.user의 기존 타입 정보(데이터베이스의 user 테이블을 반영한 TypeScript 타입)와 findMany() 메서드 인자의 타입의 교집합입니다. 명시적으로 요청된 user의 속성을 포함하는 객체 배열을 반환하며, posts는 다른 타입을 참조하는 “관계”입니다.

Python에서도 이와 유사한 작업을 수행할 수 있기를 바랍니다. 데이터베이스 스키마가 다음과 같이 Python으로 정의되어 있거나(또는 데이터베이스에서 코드가 생성되어 있거나) 하다고 가정하십시오.:

class Comment:
    id: Property[int]
    name: Property[str]
    poster: Link[User]


class Post:
    id: Property[int]

    title: Property[str]
    content: Property[str]

    comments: MultiLink[Comment]
    author: Link[User]


class User:
    id: Property[int]

    name: Property[str]
    age: Property[int | None]
    email: Property[str]
    posts: Link[Post]

(이 예에서 Property는 스칼라 타입을 나타내고, Link는 다른 테이블에 대한 참조를 나타내며, MultiLink는 다른 테이블에 대한 잠재적인 다대다 참조를 나타냅니다. 이들은 모두 ORM 라이브러리에서 정의됩니다.)

그러면 Python 코드에서 다음과 같은 호출은:

db.select(
    User,
    name=True,
    email=True,
    posts=True,
)

list[<User>]라는 동적으로 계산된 반환 타입을 가지며, 여기서:

class <User>:
    name: str
    email: str
    posts: list[<Post>]

class <Post>:
    id: int
    title: str
    content: str

더 나아가 IDE는 db.select() 호출의 모든 인자에 대해 실제 데이터베이스 열 이름과 일치하는 코드 완성을 재귀적으로 제공할 수 있습니다.

(이를 구현하는 예제 코드는 아래에 있습니다.)

FastAPI CRUD 모델 자동 도출

FastAPI tutorial에서는 간단한 Hero 타입을 위한 CRUD 엔드포인트를 빌드하는 방법을 보여 줍니다. 핵심은 데이터베이스 인터페이스를 정의하고 엔드포인트의 데이터에 대한 유효성 검사와 필터링을 수행하는 데 모두 사용되는 일련의 클래스 정의입니다.:

class HeroBase(SQLModel):
    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)


class Hero(HeroBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    secret_name: str


class HeroPublic(HeroBase):
    id: int


class HeroCreate(HeroBase):
    secret_name: str


class HeroUpdate(HeroBase):
    name: str | None = None
    age: int | None = None
    secret_name: str | None = None

HeroPublic 타입은 읽기 엔드포인트의 반환 타입으로 사용되며(출력되는 동안 유효성 검사가 수행되고 추가 필드도 제거됩니다), HeroCreateHeroUpdate는 입력 타입으로 사용됩니다(JSON에서 자동으로 변환되고 타입에 따라 유효성이 검사되며, Pydantic을 사용합니다).

여러 타입과 중복이 존재하지만, 이러한 타입을 도출하기 위한 기계적인 규칙을 작성할 수 있습니다.

  • “Public” 버전에는 “hidden”이 아닌 모든 필드를 포함하고, 기본 키는 선택 사항이 아니도록 만들어야 합니다.
  • “Create”에는 기본 키를 제외한 모든 필드를 포함해야 합니다.
  • “Update”에는 기본 키를 제외한 모든 필드를 포함하되, 모든 필드를 선택 사항으로 만들고 기본값을 지정해야 합니다.

FastAPI 프레임워크 내부에 적절한 헬퍼를 정의하면, 이 제안으로 사용자가 작성할 수 있게 됩니다.:

class Hero(NewSQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)

    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)

    secret_name: str = Field(hidden=True)

type HeroPublic = Public[Hero]
type HeroCreate = Create[Hero]
type HeroUpdate = Update[Hero]

해당 타입을 평가하면 다음과 비슷한 형태가 됩니다.:

class HeroPublic:
    id: int
    name: str
    age: int | None


class HeroCreate:
    name: str
    age: int | None = None
    secret_name: str


class HeroUpdate:
    name: str | None = None
    age: int | None = None
    secret_name: str | None = None

Public[], Create[], Update[] 계산 타입의 구현은 비교적 복잡하지만, 이 타입들은 상당히 기계적인 작업을 수행하므로 프레임워크 라이브러리에 포함하면 FastAPI 사용자가 유지 관리해야 하는 상용구를 크게 줄일 수 있습니다.

이 사용 사례의 주목할 만한 특징은 타입 어노테이션의 runtime evaluation을 수행해야 한다는 점입니다. FastAPI는 엔드포인트의 입력과 출력 모두에 대해 Pydantic 모델을 사용하여 JSON을 기준으로 유효성을 검사하고 변환합니다.

현재 이 작업의 실행 시간 측면은 수행할 수 있습니다. 원하는 규칙에 따라 실행 시간에 Pydantic 모델을 생성하는 함수를 작성할 수 있습니다. 그러나 이는 만족스럽지 않습니다. 함수를 적절하게 정적으로 타입 검사할 수 없기 때문입니다.

(이를 구현하는 예제 코드는 below에 있습니다.)

dataclasses 스타일 메서드 생성

또한 객체의 속성을 바탕으로 메서드 시그니처를 생성할 수 있기를 원합니다. 가장 잘 알려진 예는 dataclasses의 __init__ 메서드를 생성하는 것이며, 여기에서는 이를 단순화한 예를 제시합니다.

이러한 종류의 패턴은 기존 라이브러리가 수행하는 작업 중 최소 공통 분모에 해당하는 부분집합을 나타내기 위해 PEP 681이 만들어졌을 정도로 널리 퍼져 있습니다.

라이브러리가 타입 시스템에서 이러한 패턴을 더 많이 직접 구현할 수 있게 하면, 추가적인 특수 처리, 타입 검사기 플러그인, 하드코딩된 지원 등이 필요하지 않으면서 더 나은 타이핑을 제공할 수 있습니다.

(이를 구현하는 예제 코드는 below에 있습니다.)

더욱 강력한 데코레이터 타이핑

데코레이터 함수의 타이핑은 오랫동안 Python 타이핑에서 어려운 문제였습니다. 관련 PEP 612에서 ParamSpec이 도입되면서 상황이 크게 개선되었지만, 여전히 지원되지 않는 여러 패턴이 남아 있습니다.

  • 키워드 매개변수 추가/제거/수정
  • 가변 개수의 매개변수 추가/제거/수정 (여러 언패킹이 허용되고 여러 항목의 수정을 가능하게 하는 Map 연산자를 Pyre가 구현한다면, TypeVarTuple은 추가와 제거를 지원하는 데 거의 근접하게 됩니다.)

이 제안에서는 이러한 경우를 다룹니다.

일부 선행 조건의 명세

이 제안의 주요 부분을 활용하는 데 필요한 두 가지 하위 제안이 있습니다.

**kwargs를 위한 타입 변수 언패킹

이는 PEP 없이도 타이핑 제안으로 분리할 수 있을 가능성이 높은 사소한 제안입니다.

Unpack**kwargs에 사용하는 타입 변수 지원:

def f[K: BaseTypedDict](**kwargs: Unpack[K]) -> K:
    return kwargs

여기서 BaseTypedDict는 다음과 같이 정의됩니다.:

class BaseTypedDict(typing.TypedDict):
    pass

그러나 그곳에는 어떤 TypedDict도 허용됩니다.

그런 다음 다음과 같은 호출이 있다고 하면:

x: int
y: list[str]
f(x=x, y=y)

K에 대해 추론되는 타입은 다음과 비슷합니다.:

TypedDict({'x': int, 'y': list[str]})

이것은 기본적으로 PEP 692의 “Using TypedDict for more precise **kwargs typing”과 PEP 646의 “Variadic Generics”에서 *args에 대한 Unpack의 동작을 결합한 것입니다.

여기서 타입을 추론할 때 타입 검사기는 가능한 경우 리터럴 타입을 추론해야 합니다. 이는 바운드에 나타나지 않는 인자와, 바운드에 나타나는 인자를 읽기 전용으로 취급하여 리터럴 타입을 추론한다는 의미입니다.

바운드에 있는 필수 항목이 아닌 항목 중 일치하는 인자가 제공되지 않은 각 항목에 대해, 해당 항목이 읽기 전용이면 제공되지 않았음을 나타내기 위해 그 타입을 Never로 추론합니다. (읽기 전용이 아닌 항목은 불변이므로 이는 읽기 전용 항목에 대해서만 수행할 수 있습니다.)

이는 그 자체로도 어느 정도 유용할 수 있지만, 타입 수준 계산을 사용하여 **kwargs 처리를 지원하기 위해 수행됩니다.

확장 호출 가능 객체

임의로 복잡한 호출 가능 객체 타입을 표현하기 위한 새로운 확장 호출 가능 객체 제안을 도입합니다. 여기서 목표는 어노테이션에 작성할 새로운 구문을 만드는 것이 아니라(그 용도로는 상당히 장황합니다), 이 PEP의 다른 기능을 사용하여 호출 가능 객체 타입을 생성하고 introspection할 수 있는 방식으로 타입을 구성하는 방법을 제공하는 것입니다.

함수 매개변수에 대한 모든 정보를 포함하는 Param 타입을 도입합니다.:

class Param[
    N: str | None,
    T,
    K: ParamKind = Literal[ParamKind.POSITIONAL_OR_KEYWORD],
    D = typing.Never,
]:
    pass

class ParamKind(enum.IntEnum):
    POSITIONAL_ONLY = 0
    POSITIONAL_OR_KEYWORD = 1
    VAR_POSITIONAL = 2
    KEYWORD_ONLY = 3
    VAR_KEYWORD = 4

type PosParam[T] = Param[None, T, Literal[ParamKind.POSITIONAL_ONLY]]
type PosDefaultParam[T] = Param[None, T, Literal[ParamKind.POSITIONAL_ONLY], T]
type DefaultParam[N: str, T] = Param[N, T, Literal[ParamKind.POSITIONAL_OR_KEYWORD], T]
type NamedParam[N: str, T] = Param[N, T, Literal[ParamKind.KEYWORD_ONLY]]
type NamedDefaultParam[N: str, T] = Param[N, T, Literal[ParamKind.KEYWORD_ONLY], T]
type ArgsParam[T] = Param[None, T, Literal[ParamKind.VAR_POSITIONAL]]
type KwargsParam[T] = Param[None, T, Literal[ParamKind.VAR_KEYWORD]]

ParamKind 타입의 인자 K는 해당 매개변수의 종류를 나타내며, 일반적인 Literal[ParamKind.POSITIONAL_OR_KEYWORD]로 기본 설정됩니다. ParamKindinspect._ParameterKind를 미러링합니다. 여러 종류를 서로 유니언한 Param을 포함하는 Callable을 생성하는 것은 오류입니다.

인자 D는 매개변수에 기본값이 있는 경우 그 기본값의 타입을 전달하고, 그렇지 않으면 Never입니다. 기본값이 리터럴인 경우(예: None, 정수, 문자열, 열거형 멤버), D는 해당 값을 담은 Literal일 수 있습니다. 이를 그러한 Literal로 만드는 것은 introspection에 사용할 수 있고 잠재적으로 진단에 활용할 수 있게 하는 것 외에는 아무런 효과가 없습니다.

또한 Param 타입의 시퀀스를 감싸며 Callable의 첫 번째 인자 역할을 하는 Params 타입을 도입합니다.:

class Params[*Ps]:
    pass

Callable의 첫 번째 인자로 Params가 존재하면 확장 호출 가능 객체 형식과 표준 형식을 구분할 수 있습니다. ParamsParamSpec의 자연스러운 바운드 역할도 합니다.

그러면 다음과 같은 함수의 타입을 표현할 수 있습니다.:

def func(
    a: int,
    /,
    b: int,
    c: int = 0,
    *args: int,
    d: int,
    e: int = 0,
    **kwargs: int
) -> int:
    ...

다음과 같이 표현할 수 있습니다.:

Callable[
    Params[
        Param[Literal["a"], int, Literal[ParamKind.POSITIONAL_ONLY]],
        Param[Literal["b"], int],
        Param[Literal["c"], int, Literal[ParamKind.POSITIONAL_OR_KEYWORD], Literal[0]],
        Param[None, int, Literal[ParamKind.VAR_POSITIONAL]],
        Param[Literal["d"], int, Literal[ParamKind.KEYWORD_ONLY]],
        Param[Literal["e"], int, Literal[ParamKind.KEYWORD_ONLY], Literal[0]],
        Param[None, int, Literal[ParamKind.VAR_KEYWORD]],
    ],
    int,
]

또는 제공되는 타입 약어를 사용하면 다음과 같이 표현할 수 있습니다(단, 이 버전은 기본값의 구체적인 값을 추적하지 않습니다).:

Callable[
    Params[
        PosParam[Literal["a"], int],
        Param[Literal["b"], int],
        DefaultParam[Literal["c"], int],
        ArgsParam[int],
        NamedParam[Literal["d"], int],
        NamedDefaultParam[Literal["e"], int],
        KwargsParam[int],
    ],
    int,
]

이유는 below에서 논의합니다.

명세

위 예제에서 볼 수 있듯이 유효한 타입에 대한 몇 가지 새로운 구문 형식을 도입하지만, 기능의 상당 부분은 typing 모듈에 정의될 타입 수준 연산자에서 나옵니다.

타입 언어 확장의 문법 명세

Python 문법의 변경은 제안하지 않으며, Python 표현식 중 유효한 타입으로 간주되는 것의 문법만 변경합니다.

<type> = ...
     # Type booleans are all valid types too
     | <type-bool>

     # Conditional types
     | <type> if <type-bool> else <type>

     # Types with variadic arguments can have
     # *[... for t in ...] arguments
     | <ident>[<variadic-type-arg> +]

     # Type member access
     | <type>.<name>

     | GenericCallable[<type>, lambda <args>: <type>]

# Type conditional checks are boolean compositions of
# boolean type operators
<type-bool> =
      <bool-operator>[<type> +]
    | not <type-bool>
    | <type-bool> and <type-bool>
    | <type-bool> or <type-bool>
    | any(<type-bool-for>)
    | all(<type-bool-for>)

<variadic-type-arg> =
      <type> ,
    | * [ <type-for-iter> ] ,


<type-for> = <type> <type-for-iter>+ <type-for-if>*
<type-for-iter> =
      # Iterate over a tuple type
      for <var> in Iter[<type>]
<type-for-if> =
      if <type-bool>

다음과 같습니다.

  • <bool-operator>Boolean Operators 절에 정의된 이름 중 하나를 가리키며, 직접 사용하거나 정규화하여 사용하거나 다른 이름으로 사용하는 경우를 모두 포함합니다.
  • <type-bool-for>는 결과 타입이 <type>이 아닌 <type-bool>이라는 점을 제외하면 <type-for>와 동일합니다.

세 가지 반의 핵심 구문 기능이 도입됩니다. 타입 불리언, 조건부 타입, 언패킹된 컴프리헨션 타입, 타입 멤버 접근입니다.

“Generic callables”은 기술적으로 구문 기능이기도 하지만, 연산자로 논의합니다.

타입 불리언

타입 불리언은 조건문의 본문에서 사용할 수 있는 타입 언어의 특수한 부분집합입니다. 타입 불리언은 아래에 정의된 Boolean Operators로 구성되며, and, or, not, all, any와 결합될 수도 있습니다. allany의 경우, 인자는 타입 불리언의 컴프리헨션이며 unpacked comprehensions와 동일한 방식으로 평가됩니다.

타입 어노테이션 컨텍스트에서 평가되면 Literal[True]또는 Literal[False]와 동등합니다.

런타임에 해당 연산자들이 적절한 동작을 하는 “타입” 값을 생성할 수 있도록 조건문에서 사용할 수 있는 연산자를 제한하며, 기존의 Literal[False]값 등을 변경할 필요가 없도록 합니다.

조건부 타입

true_typ if bool_typ else false_typ타입은 조건부 타입이며, bool_typLiteral[True]와 동등하면 true_typ으로 해석되고, 그렇지 않으면 false_typ으로 해석됩니다.

bool_typ은 타입이지만, 위에서 정의한 타입 불리언이어야 합니다.

압축 해제 컴프리헨션

압축 해제 컴프리헨션인 *[ty for t in Iter[iter_ty]]은 현재 Unpack[...]이 허용되는 타입의 어느 위치에나 나타날 수 있으며, 본질적으로 튜플 타입 iter_ty의 인자를 순회하는 리스트 컴프리헨션이 생성한 튜플에 대한 Unpack으로 평가됩니다.

컴프리헨션에는 if 절도 포함할 수 있으며, 일반적인 방식으로 필터링합니다.

타입 멤버 접근

클래스 멤버와 함수 매개변수를 나타내기 위해 도입된 MemberParam타입에는 “연관된” 타입 멤버가 있으며, 점 표기법인 m.name, m.type 등으로 접근할 수 있습니다.

이 연산은 유니온 타입에 대해 들어 올려지지 않습니다. 잘못된 종류의 타입에 사용하면 오류가 발생합니다. 런타임에서도 반드시 그렇게 동작해야 하며, 타입 검사도 이에 맞추고자 합니다.

타입 연산자

타입 연산자는 타입 조작의 핵심 엔진이며, 타입을 분해하고 새로운 타입을 구성하는 데 사용되는 기본 연산을 제공합니다.

이 절에서는 도입되는 연산자를 정의하고, 타입 검사 컨텍스트 또는 타입 평가 컨텍스트에서 이를 평가하는 방법을 설명합니다. 다만 실제로 도입되는 런타임 클래스는 일반 클래스일 뿐이며, 여기에 첨자를 붙이면 일반적인 typing 제네릭 별칭 객체가 생성됩니다. 단, 불리언 연산자와 Iter는 부분적으로 예외이며, 이들은 runtime hooks를 위해 일부 더블 언더스코어 메서드가 오버로드된 별칭을 생성합니다.

지정된 연산자 중 다수에는 피연산자의 일부에 대한 타입 경계가 나열되어 있습니다. 이는 정확한 타입 경계라기보다 문서로 해석해야 합니다. 잘못된 인자로 연산자를 평가하려고 하면 오류가 발생합니다. 이 경우 실패한 연산자의 값은 Any이므로, 이후 평가에서 추가 오류가 연쇄적으로 발생하지 않습니다. 잠재적인 대안에 대한 논의는 아래에 일부 있습니다.

아래의 일부 경계에서 Literal[int]과 같은 표기를 사용하여 “타입이 int인 리터럴”을 의미한다는 점에 유의하십시오. 아직 이를 실제 구문으로 추가하자고 제안하는 것은 아닙니다.

불리언 연산자

  • IsAssignable[T, S]: TS에 할당할 수 있는지를 나타내는 불리언 리터럴 타입을 반환합니다.

    즉, 이는 “일관된 서브타입”입니다. 이는 점진적 타입으로 확장된 서브타이핑입니다.

  • IsEquivalent[T, S]: IsAssignable[T, S] and IsAssignable[S, T]와 동치입니다. 기술적으로 이 관계는 동치가 아니라 typing 사양에서 말하는 “일관성”입니다.
  • Bool[T]: TLiteral[True]이면 Literal[True]를 반환합니다. IsAssignable[T, Literal[True]] and not IsAssignable[T, Never]와 동치입니다.

    이는 불리언 리터럴 타입을 반환하는 “도우미 별칭”을 호출할 때 유용합니다.

기본 연산자

  • GetArg[T, Base, Idx: Literal[int]]: Base로 해석할 때 TIdx번째 타입 인자를 반환하거나, 그렇게 해석할 수 없거나 인덱스가 유효하지 않으면 타입 오류를 생성합니다. (즉, class A(B[C]): ...가 있다면 GetArg[A, B, Literal[0]] == C이고, GetArg[A, A, Literal[0]]은 타입 오류입니다.)

    TAny이면 결과는 Any입니다.

    음수 인덱스는 일반적인 방식으로 작동합니다.

    (타입 어노테이션의 런타임 평가기는 프로토콜을 Base로 사용하는 데 어려움을 겪을 가능성이 높다는 점에 유의하십시오. 예를 들어, 이터러블한 무언가의 타입을 가져오기 위한 GetArg[Ty, Iterable, Literal[0]]는 타입의 런타임 평가기에서 실패할 수 있습니다.)

    특수 형식은 특별히 처리해야 합니다. Callable의 인자 목록은 Params에 패킹되고, ...*args: Any**kwargs: Any로 처리되며, 새로운 Param 타입으로 표현됩니다.

  • GetArgs[T, Base]: Base로 해석할 때 T의 모든 타입 인자를 포함하는 튜플을 반환하거나, 그렇게 할 수 없으면 오류를 반환합니다.

    TAny이면 결과는 Any입니다.

  • Length[T: tuple] - 튜플의 길이를 int 리터럴로 가져옵니다 (길이가 제한되지 않으면 Literal[None]을 반환합니다).
  • Slice[S: tuple, Start: Literal[int | None], End: Literal[int | None]]: 튜플 타입을 슬라이스합니다.

    SAny이면 결과는 Any입니다.

  • GetSpecialAttr[T, Attr: Literal[str]]: 클래스 T에서 Attr라는 이름의 특수 속성 값을 추출합니다. 유효한 속성은 __name__, __module____qualname__입니다. 값을 Literal[str]로 반환합니다.

    클래스가 아닌 타입의 경우, x가 타입 T라면 type(x).<attr>의 값을 원한다는 것이 기본 원칙이어야 합니다. 따라서 GetSpecialAttr[Literal[1], "__name__"]Literal["int"]를 생성해야 합니다.

이 절의 모든 연산자는 lifted over union types에 대해 승격됩니다.

유니온 처리

  • FromUnion[T]: 합집합의 모든 요소를 포함하는 튜플을 반환하거나, 합집합이 아닌 경우 T를 포함하는 1항 튜플을 반환합니다.
  • Union[*Ts]: Union은 가변 인자를 받을 수 있게 되므로, 압축 해제된 컴프리헨션 인자를 받을 수 있게 됩니다.

객체 검사

  • Members[T]: 클래스 또는 타입 딕셔너리 T의 멤버(속성 및 메서드)를 설명하는 Member 타입의 tuple을 생성합니다.

    타입 검사 시점과 런타임 평가가 더욱 긴밀하게 일치하도록 명시적 타입 어노테이션이 있는 멤버만 포함합니다. (이는 어노테이션이 없는 메서드도 제외하기 위한 것이지만, 미해결 문제를 참조하십시오.)

  • Attrs[T]: Members[T]와 같지만 속성만 반환합니다(메서드는 반환하지 않음).
  • GetMember[T, S: Literal[str]]: 클래스 T에서 S라는 이름의 멤버에 대한 Member 타입을 생성하거나, 존재하지 않으면 오류를 생성합니다.

    TAny이면 결과는 Any입니다.

  • GetMemberType[T, S: Literal[str]]: 클래스 T에서 S라는 이름의 멤버 타입을 추출하며, 해당 멤버가 없으면 Never입니다.

    TAny이면 결과는 Any입니다.

  • Member[N: Literal[str], T, Q: MemberQuals, Init, D]: Member는 연산자가 아니라 클래스의 멤버를 설명하는 데 사용되는 단순한 타입입니다. 해당 타입 매개변수는 각 멤버에 대한 정보를 인코딩합니다.
    • N은 리터럴 문자열 타입으로 표현된 이름입니다. .name으로 접근할 수 있습니다.
    • T는 타입입니다. .type으로 접근할 수 있습니다.
    • Q는 수식자의 합집합입니다(아래의 MemberQuals를 참조하십시오). .quals로 접근할 수 있습니다. 수식자가 없으면 Never입니다.
    • Init은 클래스에서 속성 초기화자의 리터럴 타입입니다(InitField를 참조하십시오). .init으로 접근할 수 있습니다. 초기화자가 없으면 Never입니다.
    • D는 멤버를 정의하는 클래스입니다. (즉, 멤버가 상속된 클래스입니다. TypedDict의 경우에는 항상 Never입니다). .definer로 접근할 수 있습니다.
  • MemberQuals = Literal['ClassVar', 'Final', 'NotRequired', 'ReadOnly'] - MemberQuals는 멤버에 적용할 수 있는 “수식자”의 타입입니다. 현재 ClassVarFinal은 클래스에 적용되고, NotRequiredReadOnly는 타입 딕셔너리에 적용됩니다.

메서드는 클래스 본문에 정의된 함수, 정적 메서드 및 클래스 메서드입니다. 프로퍼티는 속성으로 취급해야 합니다.

메서드는 Param-기반 확장 호출 가능 객체로 검사할 수 있는 호출 가능 객체로 반환되며, ClassVar수식자를 가집니다. staticmethodclassmethodstaticmethodclassmethod타입을 반환하며, 이러한 타입은 Python 3.14부터 첨자 표기할 수 있습니다.

이 절의 모든 연산자는 합집합 타입에 대해 승격됩니다.

객체 생성

  • NewProtocol[*Ms: Member]: Member인수로 지정된 멤버를 포함하는 새로운 구조적 프로토콜을 생성합니다.
  • NewTypedDict[*Ps: Member] - Member인수로 지정된 항목을 포함하는 새로운 TypedDict를 생성합니다.

현재 명목적 클래스를 생성하거나 새로운 제네릭 타입을 만드는 방법은 제안하지 않는다는 점에 유의하십시오.

InitField

dataclasses/attrs/Pydantic 스타일의 필드 디스크립터를 기반으로 타입을 변환할 수 있도록 지원하고자 합니다. 그러기 위해서는 Field를 호출하는 것과 같은 연산을 처리할 수 있어야 합니다.

이를 위해 KwargDict: TypedDict에 정의된 인수를 수집하는 새로운 타입 InitField[KwargDict]를 도입하는 전략을 사용합니다.:

class InitField[KwargDict: BaseTypedDict]:
    def __init__(self, **kwargs: typing.Unpack[KwargDict]) -> None:
        ...

    def _get_kwargs(self) -> KwargDict:
        ...

InitField또는 (더 가능성이 높은 경우로) 그 서브타입이 클래스 본문 내부에서 인스턴스화되면, 가능한 경우 Literal타입을 기반으로 이에 대한 더 구체적인 타입을 추론합니다. (실제로는 **kwargs에서 타입 변수 언패킹에 Literal타입을 사용해야 한다는 규칙을 적용한 것입니다.)

따라서 다음과 같이 작성하면:

class A:
    foo: int = InitField(default=0, kw_only=True)

초기화자에 대해 InitField[TypedDict('...', {'default': Literal[0], 'kw_only': Literal[True]})]타입을 추론하며, 이는 MemberInit필드로 사용할 수 있게 됩니다.

호출 가능 객체 검사 및 생성

Callable 타입은 항상 위에서 논의한 확장된 호출 가능 객체 형식으로 인자를 노출합니다.

Member와 연관된 타입 이름은 .name.type입니다. Param에는 .kind라는 연관 타입도 있으며, 이는 K를 노출하고 .default라는 연관 타입은 D를 노출합니다.

제네릭 호출 가능 객체

  • GenericCallable[Vs, lambda <vs>: Ty]: 제네릭 호출 가능 객체입니다. Vs는 바인딩되지 않은 타입 변수의 튜플 타입이며, TyVs의 변수에 <vs>의 바인딩된 변수를 통해 액세스할 수 있는 Callable, staticmethod 또는 classmethod이어야 합니다.

현재는 GenericCallable의 사용을 Member의 타입 인자로 제한하여 로컬 변수, 매개변수 타입, 반환 타입, 다른 타입 내부 등에 사용할 수 없도록 합니다. 근거는 below에서 설명합니다.

오버로드된 함수 타입

  • Overloaded[*Callables] - 기반 타입을 순서대로 포함하는 오버로드된 함수 타입입니다.

오류 발생

  • RaiseError[S: Literal[str], *Ts]: 실제 타입을 결정하기 위해 이 타입을 평가해야 하는 경우, 제공된 메시지와 함께 타입 오류를 생성합니다.

    추가 타입 인자는 모두 메시지에 포함해야 합니다.

클래스 업데이트

  • UpdateClass[*Ps: Member]: 새 멤버로 기존 명목 클래스를 업데이트하는 특수 형식입니다. 기존 멤버를 재정의하거나, 해당 멤버의 타입을 Never로 만들어 제거할 수도 있습니다.

    이는 타입 데코레이터의 반환 타입 또는 __init_subclass__의 반환 타입으로만 사용할 수 있습니다.

    클래스를 선언할 때 하나 이상의 상위 클래스가 UpdateClass 반환 타입을 갖는 __init_subclass__를 보유하고 있으면, 해당 업데이트를 역순 MRO 순서로 적용합니다. cls매개변수가 type[T]로 매개변수화되어 있으면, 클래스 타입을 T에 대입해야 합니다.

유니언에 대한 리프팅

많은 내장 연산은 Union에 대해 리프트됩니다.

예를 들어:

Slice[tuple[A, B, C], Literal[0] | Literal[1], Literal[2] | Literal[3]] = (
    tuple[A, B]
    | tuple[A, B, C]
    | tuple[B]
    | tuple[B, C]
)

연산이 유니언 타입에 대해 리프트되면, 각 인자 위치에 있는 유니언 요소들의 데카르트 곱을 구하고, 그 데카르트 곱의 각 튜플에 대해 연산자를 평가한 다음, 모든 결과를 다시 유니언으로 결합합니다. Python에서는 논리가 다음과 같이 보입니다.:

args_union_els = [get_union_elems(arg) for arg in args]
results = [
    eval_operator(*xs)
    for xs in itertools.product(*args_union_els)
]
if results:
    return Union[*results]
else:
    return Never

런타임 평가 지원

중요한 목표는 이러한 계산된 타입의 런타임 평가를 지원하는 것입니다. 공식 평가기를 표준 라이브러리에 추가하는 것은 제안하지 않지만, 서드파티 평가기 라이브러리를 출시할 예정입니다.

타입 시스템 확장의 대부분은 “불활성” 타입 연산자 적용이지만, 구문에는 리스트 반복, 조건문 및 속성 액세스도 포함되며, 클래스, 별칭 또는 함수의 __annotate__ 메서드가 호출될 때 자동으로 평가됩니다.

이러한 경우에 평가기 라이브러리가 타입 평가를 트리거할 수 있도록 typing에 새 훅을 추가합니다.

  • special_form_evaluator: 이는 __bool__Boolean Operator에 대해 호출되거나 __iter__typing.Iter에 대해 호출될 때 typing._GenericAlias 인자와 함께 호출될 호출 가능 객체를 보유하는 ContextVar입니다. 그런 다음 반환하기 전에 반환된 값에 bool 또는 iter를 호출합니다.

    None으로 설정하면(기본값), 불리언 연산자는 False를 반환하고 Iteriter(())로 평가됩니다.

어노테이션을 가져오기 위한 Format.AST 모드를 추가하는 방안이 논의된 적이 있습니다(이 PEP draft를 참조하십시오). 그렇게 하면 완전히 평가되지 않은 어노테이션을 계속 쉽게 가져올 수 있으므로 이 제안과 매우 잘 어울립니다.

예제 / 튜토리얼

여기서는 동기 부여 섹션의 예제에서 목표를 달성하는 방법을 설명하고, 사용하면서 해당 기능을 설명하는 튜토리얼 방식으로 접근합니다.

Prisma 스타일 ORM

먼저 위에서 살펴본 어노테이션을 지원하기 위해, 제네릭 타입을 사용하는 더미 클래스 모음이 있습니다.

class Pointer[T]:
    pass

class Property[T](Pointer[T]):
    pass

class Link[T](Pointer[T]):
    pass

class SingleLink[T](Link[T]):
    pass

class MultiLink[T](Link[T]):
    pass

select 메서드에서 새로운 요소가 나타나기 시작합니다.

**kwargs: Unpack[K]는 이 제안의 일부이며, 키워드 인자에서 TypedDict를 추론할 수 있게 합니다.

Attrs[K]K의 타입 어노테이션이 지정된 모든 속성에 대응하는 Member 타입을 추출하는 한편, Member 인자를 사용해 NewProtocol을 호출하면 새로운 구조적 타입을 구성합니다.

c.name은 변수 c에 바인딩된 Member의 이름을 리터럴 타입으로 가져옵니다. 이러한 모든 메커니즘은 리터럴 타입에 크게 의존합니다. GetMemberType은 클래스에서 속성의 타입을 가져옵니다.

def select[ModelT, K: typing.BaseTypedDict](
    typ: type[ModelT],
    /,
    **kwargs: Unpack[K],
) -> list[
    typing.NewProtocol[
        *[
            typing.Member[
                c.name,
                ConvertField[typing.GetMemberType[ModelT, c.name]],
            ]
            for c in typing.Iter[typing.Attrs[K]]
        ]
    ]
]:
    raise NotImplementedError

ConvertField는 첫 번째 타입 헬퍼이며, 제한적인 서브타입 유사 검사를 바탕으로 두 타입 중 하나를 결정하는 조건부 타입 별칭입니다.

ConvertField에서는 Property 또는 Link 어노테이션을 제거하고 기반 타입을 생성하려고 합니다. 또한 링크의 경우 속성만 포함하는 새 대상 타입을 생성하고 MultiLink를 리스트로 감쌉니다.

type ConvertField[T] = (
    AdjustLink[PropsOnly[PointerArg[T]], T]
    if typing.IsAssignable[T, Link]
    else PointerArg[T]
)

PointerArgPointer 또는 그 서브클래스의 타입 인자를 가져옵니다.

GetArg[T, Base, I]는 핵심 기본 요소 중 하나입니다. T에서 Base의 인덱스 I 타입 인자를 가져오며, 이는 TBase를 상속하는 경우에 해당합니다.

(이 기능의 세부 사항은 나중에 설명하겠습니다. 이 경우에는 Pointer의 인자만 가져옵니다.)

type PointerArg[T] = typing.GetArg[T, Pointer, Literal[0]]

AdjustLink는 앞에서 논의한 기능을 사용하여 MultiLinklist로 감쌉니다.

type AdjustLink[Tgt, LinkTy] = (
    list[Tgt] if typing.IsAssignable[LinkTy, MultiLink] else Tgt
)

마지막 헬퍼인 PropsOnly[T]T의 모든 Property 속성을 포함하는 새 타입을 생성합니다.

type PropsOnly[T] = typing.NewProtocol[
    *[
        typing.Member[p.name, PointerArg[p.type]]
        for p in typing.Iter[typing.Attrs[T]]
        if typing.IsAssignable[p.type, Property]
    ]
]

전체 테스트는 테스트 스위트에 있습니다.

FastAPI CRUD 모델 자동 파생

테스트 스위트에는 더 완전한 예제가 있지만, 여기서는 Create만 구현하는 가능한 방법을 보여 줍니다.:

# Extract the default type from an Init field.
# If it is a Field, then we try pulling out the "default" field,
# otherwise we return the type itself.
type GetDefault[Init] = (
    GetFieldItem[Init, Literal["default"]]
    if typing.IsAssignable[Init, Field]
    else Init
)

# Create takes everything but the primary key and preserves defaults
type Create[T] = typing.NewProtocol[
    *[
        typing.Member[
            p.name,
            p.type,
            p.quals,
            GetDefault[p.init],
        ]
        for p in typing.Iter[typing.Attrs[T]]
        if not typing.IsAssignable[
            Literal[True],
            GetFieldItem[p.init, Literal["primary_key"]],
        ]
    ]
]

Create 타입 별칭은 원래 타입의 속성을 순회하여 새 타입을 생성합니다(NewProtocol을 통해). 이 별칭은 이름, 타입, 한정자 및 초기화자의 리터럴 타입에 접근할 수 있습니다. 이는 여기서 사용한 매우 일반적인 = Field(...)-와 유사한 패턴을 처리하기 위한 새로운 기능을 부분적으로 활용합니다.

여기서는 Field에서 primary_key=True인 속성을 제외하고, 기본 인자도 추출합니다. 기본 인자는 필드의 default인자에서 가져오거나 초기화자로 직접 지정할 수 있습니다.

dataclasses 스타일 메서드 생성

InitFnType은 모든 속성을 순회하여 새로운 __init__ 함수에 대한 Member를 생성합니다.

여기서 GetDefault는 위의 FastAPI 스타일 예제에서 가져온 것입니다.

# Generate the Member field for __init__ for a class
type InitFnType[T] = typing.Member[
    Literal["__init__"],
    Callable[
        typing.Params[
            typing.Param[Literal["self"], T],
            *[
                typing.Param[
                    p.name,
                    p.type,
                    # All arguments are keyword-only
                    Literal[ParamKind.KEYWORD_ONLY],
                    # GetDefault is Never when there's no default, so use it
                    # directly as D.
                    GetDefault[p.init],
                ]
                for p in typing.Iter[typing.Attrs[T]]
            ],
        ],
        None,
    ],
    Literal["ClassVar"],
]
type AddInit[T] = typing.NewProtocol[
    InitFnType[T],
    *[x for x in typing.Iter[typing.Members[T]]],
]

그런 다음 UpdateClass를 사용하여 클래스 데코레이터(@dataclass와 같은)를 만들고 클래스에 새로운 __init__ 메서드를 추가할 수 있습니다.

def dataclass_ish[T](
    cls: type[T],
) -> typing.UpdateClass[
    # Add the computed __init__ function
    InitFnType[T],
]:
    raise NotImplementedError

또는 그렇게 동작하는 베이스 클래스(Pydantic과 같은)를 생성하는 것입니다.

class Model:
    def __init_subclass__[T](
        cls: type[T],
    ) -> typing.UpdateClass[
        # Add the computed __init__ function
        InitFnType[T],
    ]:
        pass

zip 스타일 함수

타입 순회와 GetArg를 사용하면 zip에 적절한 타입을 부여할 수 있습니다.

type ElemOf[T] = typing.GetArg[T, Iterable, Literal[0]]

def zip[*Ts](
    *args: *Ts, strict: bool = False
) -> Iterator[tuple[*[ElemOf[t] for t in typing.Iter[tuple[*Ts]]]]]:
    return builtins.zip(*args, strict=strict)  # type: ignore[call-overload]

Slice 연산자와 타입 별칭 재귀를 사용하면 서로 다른 타입의 튜플을 함께 압축하는 작업에 더 정확한 타입도 부여할 수 있습니다.

예를 들어 tuple[int, str]tuple[str, bool]를 압축하면 tuple[tuple[int, float], tuple[str, bool]]를 생성해야 합니다.

def zip_pairs[*Ts, *Us](
    a: tuple[*Ts], b: tuple[*Us]
) -> Zip[tuple[*Ts], tuple[*Us]]:
    return cast(
        Zip[tuple[*Ts], tuple[*Us]],
        tuple(zip(a, b, strict=True)),
    )

type DropLast[T] = typing.Slice[T, Literal[0], Literal[-1]]
type Last[T] = typing.GetArg[T, tuple, Literal[-1]]

# Matching on Never here is intentional; it prevents infinite
# recursions when T is not a tuple.
type Empty[T] = typing.IsAssignable[typing.Length[T], Literal[0]]

Zip은 입력 튜플 중 하나 또는 둘 다가 비워질 때까지 입력 튜플을 재귀적으로 순회합니다. 길이가 일치하지 않으면(하나만 비어 있기 때문입니다) 오류를 발생시키십시오.

type Zip[T, S] = (
    tuple[()]
    if typing.Bool[Empty[T]] and typing.Bool[Empty[S]]
    else typing.RaiseError[Literal["Zip length mismatch"], T, S]
    if typing.Bool[Empty[T]] or typing.Bool[Empty[S]]
    else tuple[*Zip[DropLast[T], DropLast[S]], tuple[Last[T], Last[S]]]
)

TypeScript 스타일 “유틸리티 타입”

TypeScript는 일반적인 타입 연산을 수행하기 위한 여러 유틸리티 타입을 정의합니다.

그중 일부의 구현을 제시합니다.:

# Pick<T, Keys>
# Constructs a type by picking the set of properties Keys from T.
type Pick[T, Keys] = typing.NewProtocol[
    *[
        p
        for p in typing.Iter[typing.Members[T]]
        if typing.IsAssignable[p.name, Keys]
    ]
]

# Omit<T, Keys>
# Constructs a type by picking all properties from T and then removing Keys.
# Note that unlike in TS, our Omit does not depend on Exclude.
type Omit[T, Keys] = typing.NewProtocol[
    *[
        p
        for p in typing.Iter[typing.Members[T]]
        if not typing.IsAssignable[p.name, Keys]
    ]
]

# KeyOf[T]
# Constructs a union of the names of every member of T.
type KeyOf[T] = Union[*[p.name for p in typing.Iter[typing.Members[T]]]]

# Exclude<T, U>
# Constructs a type by excluding from T all union members assignable to U.
type Exclude[T, U] = Union[
    *[
        x
        for x in typing.Iter[typing.FromUnion[T]]
        if not typing.IsAssignable[x, U]
    ]
]

# Extract<T, U>
# Constructs a type by extracting from T all union members assignable to U.
type Extract[T, U] = Union[
    *[
        x
        for x in typing.Iter[typing.FromUnion[T]]
        # Just the inverse of Exclude, really
        if typing.IsAssignable[x, U]
    ]
]

# Partial<T>
# Constructs a type with all properties of T set to optional (T | None).
type Partial[T] = typing.NewProtocol[
    *[
        typing.Member[p.name, p.type | None, p.quals]
        for p in typing.Iter[typing.Attrs[T]]
    ]
]

# PartialTD<T>
# Like Partial, but for TypedDicts: wraps all fields in NotRequired
# rather than making them T | None.
type PartialTD[T] = typing.NewTypedDict[
    *[
        typing.Member[p.name, p.type, p.quals | Literal["NotRequired"]]
        for p in typing.Iter[typing.Attrs[T]]
    ]
]

근거

확장 호출 가능 객체

타입 수준 계산을 통해 호출 가능 객체를 검사하고 생성하려면 확장 호출 가능 객체 지원이 필요합니다. mypy는 확장 호출 가능 객체를 지원하지만, 이는 콜백 프로토콜을 사용하는 방식으로 더 이상 사용되지 않습니다.

안타깝게도 콜백 프로토콜은 타입 수준 계산에 잘 맞지 않습니다. (작동하도록 만들 수는 있겠지만, 메서드를 생성하고 내부 검사하기 위한 별도의 기능이 필요하며, 이는 더 간단하지 않을 것입니다.)

다음과 같은 이유로 완전히 새로운 확장 호출 가능 객체 구문을 제안합니다.
  1. mypy_extensions 함수는 완전히 아무 작업도 하지 않으며, 실제 런타임 객체가 필요합니다.
  2. 이 함수들은 대괄호가 아니라 괄호를 사용하므로, 여기의 철학에 크게 어긋납니다.
  3. 멤버를 검사하기 위해 수행하려는 작업에 더 잘 부합하는 API를 만들 수 있습니다. (새로운 방식이 처음부터 불가능한 것은 아니라면, mypy_extensions 버전을 긴밀하게 모방하는 확장 호출 가능 객체를 도입할 수도 있습니다.)

제네릭 호출 가능 객체

다음 시그니처를 갖는 메서드를 생각해 보십시오.:

def process[T](self, x: T) -> T if IsAssignable[T, list] else list[T]:
    ...

메서드의 타입은 제네릭이며, 제네릭은 클래스가 아니라 메서드에 바인딩됩니다. 프로그래머가 NewProtocol에 작성할 법한 방식으로 이러한 제네릭 함수를 표현할 방법이 필요합니다.

다소 매력적이지만 작동하지 않는 한 가지 방법은 바인딩되지 않은 타입 변수를 사용하고 이를 일반화하도록 두는 것입니다.:

type Foo = NewProtocol[
    Member[
        Literal["process"],
        Callable[[T], T if IsAssignable[T, list] else list[T]]
    ]
]

문제는 이것이 런타임 평가 지원과 기본적으로 호환되지 않는다는 점입니다. Foo를 평가하려면 IsAssignable을 평가해야 하므로, 적어도 조건문의 한쪽을 잃게 됩니다. 제네릭 함수를 포함하는 클래스에서 Members를 평가할 때도 비슷한 문제가 발생합니다. 본문을 람다로 감싸면 이 두 경우 모두 평가를 지연할 수 있습니다. (Members의 경우 평가를 지연하는 방식은 명시적 제네릭 어노테이션이 있는 함수에서 꽤 잘 작동합니다. 이전 스타일 제네릭의 경우에는 변수를 만났을 때 오류를 발생시키도록 먼저 평가를 시도해야 할 것입니다.)

실제 구문을 사용하면 다음과 같이 표현됩니다.:

type Foo = NewProtocol[
    Member[
        Literal["process"],
        GenericCallable[
            tuple[T],
            lambda T: Callable[[T], T if IsAssignable[T, list] else list[T]],
        ],
    ]
]

GenericCallable의 사용을 Member의 타입 인자로 제한하자고 제안하는 이유는 타입 추론과 결합될 때, impredicative 다형성(타입 변수를 다른 제네릭 타입으로 인스턴스화할 수 있는 경우)과 rank-N 타입(함수 타입 내부 깊숙한 중첩 위치에서 제네릭을 바인딩할 수 있는 경우)이 해결하기 매우 까다로운 문제가 되기 때문입니다 [1]. 이를 지원하면 좋겠지만, 지금은 그 문제를 꺼내고 싶지 않습니다.

바인딩되지 않은 타입 변수 튜플을 사용하는 이유는 범위, 기본값 및 TypeVarTuple여부를 지정할 수 있도록 하기 위해서이지만, 새로운 접근 방식을 고안하고 싶을 수도 있습니다.

하위 호환성

가장 엄격한 의미에서 이 PEP는 새로운 기능만 제안하므로 하위 호환성 문제가 없어야 합니다.

다만 좀 더 넓은 의미에서 보면, 타입 어노테이션에 iffor를 사용하면 타입 어노테이션을 추출하려는 도구에 문제가 발생할 수 있습니다.

어노테이션을 완전히 평가하려는 도구는 평가기를 구현하거나 이를 위한 라이브러리를 사용해야 합니다. (PEP 작성자들은 그러한 라이브러리를 제작할 계획입니다.)

런타임에 어노테이션을 구체적으로 내부 검사하는 데 의존하는 도구(파이썬 파일을 구문 분석하는 도구는 분명히 영향을 받지 않습니다) 중 평가되지 않은 어노테이션을 추출하여 어떤 방식으로든 처리하려는 도구는 더 큰 문제에 직면할 가능성이 있습니다. 현재는 from __future__ import annotations가 지정된 경우 이를 수행할 수 있습니다. 문자열 어노테이션을 ast.parse로 구문 분석한 다음 임의의 방식으로 처리할 수 있기 때문입니다.

이것이 없으면 현재 상황에서는 일이 더 복잡해집니다. 현재로서는 어노테이션을 실행하지 않고는 __annotate__ 함수에서 유용한 정보를 얻을 방법이 없으며, 문자열을 구성하기 위한 해당 함수의 기법은 반복문과 조건문에 사용할 수 없기 때문입니다.

다음 중 하나를 수행하면 이 문제를 완화할 수 있습니다.
  1. 해당 “Just store the strings” 옵션은 PEP 649에서 제공되며, 평가되지 않은 문자열을 항상 추출할 수 있게 합니다.
  2. 어노테이션을 가져오기 위한 Format.AST 모드를 추가합니다(이 PEP 초안을 참조하십시오).

앞의 두 옵션 중 어느 것도 선택하지 않는다면, 평가되지 않은 타입 조작 표현식을 처리하려는 도구는 소스 코드를 다시 구문 분석하고 그곳에서 어노테이션을 추출해야 할 가능성이 높습니다. 이는 어쨌든 대부분의 도구가 수행하는 작업이라고 예상합니다.

보안 관련 영향

예상되는 영향은 없습니다.

이 내용을 가르치는 방법

TypeScript가 비슷한 복잡성을 지닌 동등한 기능을 가르치는 방식에서 많은 영감을 얻을 수 있다고 생각합니다. TypeScript가 하는 방식과 유사한, 예제를 중심으로 한 고수준 문서가 필요할 것입니다.

새로운 구문과 API를 사용할 것으로 예상되는 대상이, 자신이 도입하는 고급 패턴과 API를 지원하기 위해 타입 조작을 구현할 프레임워크 및 라이브러리 유지 관리자라는 점도 중요합니다.

참조 구현

demo of a runtime evaluator가 있으며, 이 PEP 초안도 현재 그곳에 있습니다.

mypy에는 진행 중인 proof-of-concept implementation이 있습니다.

이 구현은 이 문서의 모든 예제에 대해 타입 검사를 수행할 수 있습니다.

대체 구문 아이디어

즉, ‘“거부된” 아이디어를 사실은 우리가 해야 하지 않을까?’

이에 대한 피드백을 매우 기다리고 있습니다!

타입이 지정된 딕셔너리와 프로토콜을 생성하기 위한 딕셔너리 컴프리헨션 기반 구문

이는 어떤 면에서는 인라인 타입 딕셔너리를 위한 PEP 764 (아직 초안 단계) 제안의 확장입니다.

위 제안과 결합하여 NewProtocol에 사용하면, 쿼리 빌더 예제의 내용을 활용할 때 대략 다음과 같은 형태가 될 수 있습니다.

type PropsOnly[T] = typing.NewProtocol[
    {
        p.name: PointerArg[p.type]
        for p in typing.Iter[typing.Attrs[T]]
        if typing.IsAssignable[p.type, Property]
    }
]

그런 다음 한정자 및/또는 초기화자 타입을 지정하려는 경우를 위해 Member를 지정할 수 있도록 하는 것도 원할 것입니다(단, Name이 마지막에 오고 기본값을 갖도록 순서를 바꿉니다).

한정자를 타입에 작성하도록 허용할 수도 있지만, 이는 타입 표현식이 아니라 어노테이션 표현식이므로 조금 이상하며, 조건부 타입의 한 분기에 어노테이션 표현식을 포함하는 것은 아마도 허용되지 않을 것입니다.

이 제안의 가장 큰 단점은 단순히 복잡성입니다. 또 다른 종류의 특이한 타입 형식을 도입해야 하기 때문입니다.

또한 TypedDict와 새 프로토콜 간의 정확한 상호 작용을 파악해야 합니다. 딕셔너리 구문이 항상 타입 딕셔너리를 생성한 다음 NewProtocol이 이를 프로토콜로 변환하도록 할까요, 아니면 NewProtocol[<dict type expr>]이 특수 형식이 되도록 할까요? ClassVarFinal을 허용할까요?

구조 분해?

또 다른 잠재적인 “단점”(실제로는 장점일 수도 있습니다)은 AttrsMembersitems()스타일의 이터레이터로 순회할 수 있어야 할지도 모른다는 점을 시사하며, 이로 인해 더 복잡한 질문이 제기됩니다.

먼저 구문은 다음과 비슷할 것입니다.:

type PropsOnly[T] = typing.NewProtocol[
    {
        k: PointerArg[ty]
        for k, ty in typing.IterItems[typing.Attrs[T]]
        if typing.IsAssignable[ty, Property]
    }
]

이는 꽤 괜찮아 보이지만, 한정자나 초기화자가 아니라 이름과 타입에만 접근할 수 있습니다.

이를 처리할 수 있는 잠재적인 방법은 다음과 같습니다.

  • 괜찮습니다. 프로그래머는 일반적인 경우에 이 .items()스타일의 이터레이터를 사용하고, 필요할 때는 전체 Member객체를 대상으로 작업할 수 있습니다.
  • 한정자/초기화자를 key에 넣을 수 있습니다. 실제로 이름을 사용하려면 key.name이나 이와 유사한 작업을 수행해야 합니다.

(이 방식으로 이터레이션할 수 있는 대상에 관한 규칙이 정확히 무엇인지도 알아내야 합니다.)

괄호를 사용하여 타입 연산자를 호출하십시오

사람들이 브래킷 시티에서 어려움을 겪고 있다면, 내장 타입 연산자에 브래킷 대신 괄호를 사용하도록 만드는 것도 고려할 수 있습니다.

[]()를 섞어 사용하면 일관성 문제가 발생하고, 사용자가 어떤 API가 대괄호를 사용하고 어떤 API가 괄호를 사용하는지 기억해야 합니다. 현재 Python 타이핑이 브래킷 사용을 중심으로 이루어져 있으므로, 그 방향을 계속 유지하는 것이 더 나은 개발자 경험으로 이어질 것이라고 강하게 확신합니다.

예를 들어, ()[]를 섞으면 다음과 같이 보일 수 있습니다.:

type PropsOnly[T] = typing.NewProtocol(
    {
        p.name: PointerArg[p.type]
        for p in typing.Iter(typing.Attrs(T))
        if typing.IsAssignable(p.type, Property)
    }
)

(사용자 정의 타입 별칭인 PointerArg는 기본적으로 도우미 연산자임에도 여전히 브래킷으로 호출해야 합니다.)

점 표기법으로 접근할 수 있는 연관 타입을 위한 일반 메커니즘을 마련하십시오

현재 주요 제안에서는 MemberParam.name.type에 대한 연관 타입을 정확히 어떻게 갖게 될지에 대해 침묵하고 있습니다.

특정 타입에 대해서만 작동하게 만들 수도 있고, 다음과 같은 일반 메커니즘을 도입할 수도 있습니다.:

@typing.has_associated_types
class Member[
    N: str,
    T,
    Q: MemberQuals = typing.Never,
    I = typing.Never,
    D = typing.Never
]:
    type name = N
    type tp = T
    type quals = Q
    type init = I
    type definer = D

런타임에 연관 타입의 점 표기법이 작동하도록 하려면 데코레이터(또는 베이스 클래스)가 필요합니다. 클래스가 생성한 typing._GenericAlias__getattr__동작을 사용자 지정하여 Member의 타입 매개변수와 별칭을 모두 캡처해야 하기 때문입니다.

(다만 이를 위해 필요할 수도 있는 작업을 피하도록 _GenericAlias자체의 동작을 변경할 수도 있습니다.)

거부된 아이디어

런타임 평가를 전혀 고려하지 않기

이렇게 하면 구문 형식을 실험할 수 있는 유연성이 커지고, 압축 해제된 컴프리헨션 타입에서 typing.Iter를 요구하거나 조건부 타입에 나타날 수 있는 <type-bool>표현식의 집합이 제한되는 등의 일부 난점도 없앨 수 있습니다.

좋든 나쁘든 타입 어노테이션의 런타임 사용은 널리 퍼져 있습니다. 예를 들어 pydantic이 이에 의존하며, 우리의 동기를 부여한 예시 중 하나인 FastAPI CRUD 모델 자동 파생도 이에 의존합니다.

서브타입 검사에서 TypeScript 스타일 패턴 매칭 지원

TypeScript에서는 조건부 타입을 다음과 같이 구성합니다.:

SomeType extends OtherType ? TrueType : FalseType

또한 검사의 우변에서는 infer키워드를 사용하여 패턴 매칭에 기반해 타입 변수를 바인딩할 수 있으며, 다음은 배열의 요소 타입을 추출하는 예시입니다.:

type ArrayArg<T> = T extends [infer El] ? El : never;

이는 매우 우아한 메커니즘이며, 특히 typing.GetArg와 그 미묘한 Base매개변수가 필요하지 않게 만든다는 점에서 그렇습니다.

안타깝게도 이를 Python의 기존 구문에 어떤 만족스러운 방식으로든 억지로 끼워 넣기는 매우 어려워 보이며, 특히 미묘한 바인딩 구조 때문에 그렇습니다.

아마도 가장 그럴듯한 변형은 다음과 같을 것입니다.:

type ArrayArg[T] = El if IsAssignable[T, list[Infer[El]]] else Never

그런 다음 이를 런타임에 평가하려면 바인딩되지 않은 Infer인자를 포착하는 사용자 정의 globals환경을 사용하는 복잡한 작업이 필요합니다.

또한 주요 구문 변경 없이(삼항 연산자 대신 타입 연산자 사용) 조건부를 유니언 위로 끌어 올리는 TypeScript의 동작을 일치시킬 수 없습니다.

IsAssignable을 “할당 가능” 검사보다 약한 것으로 대체하기

완전한 Python 타이핑 할당 가능성 검사는 런타임에 완전히 구현할 수 없습니다. (특히 표준 라이브러리의 typeshed 타입을 모두 사용할 수 있게 하더라도 프로토콜에 대한 검사는 종종 불가능합니다. 클래스 속성이 추론될 수 있고 런타임에 눈에 보이는 형태로 존재하지 않을 수 있기 때문입니다.)

제안된 방식에서 런타임 평가기는 “최선의 노력” 방식이어야 하며, 이상적으로는 그러한 노력의 범위를 잘 문서화해야 합니다.

대안적인 접근 방식은 더 약한 술어를 핵심 기본 요소로 사용하는 것입니다.

한 가지 가능성은 “부분 유사성” 검사입니다. IsAssignableSimilar은 본질적으로 타입 매개변수를 살펴보지 않고 타입의 헤드간단히 검사합니다. 이는 프로토콜에서는 작동하지 않습니다. 그래도 유니언 위로 끌어 올리며 리터럴을 검사합니다.

서브타이핑과 유사하지만 동일하지 않은 새로운 개념을 도입하는 것은 아마 좋은 생각이 아니라고 판단했습니다. 또한 IsAssignableSimilar처럼 길고 이상한 이름을 사용하거나, IsAssignable처럼 오해를 부르는 짧은 이름을 사용해야 하기 때문입니다.

Member 구성 요소에 접근할 때 점 표기법을 사용하지 마십시오.

이 PEP 초안의 이전 버전에서는 MemberParam 구성 요소에 m.name 등과 같이 작성할 수 있는 기능을 제외하고, 대신 typing.GetName과 같은 도우미 연산자에 의존했습니다(이는 내부적으로 typing.GetArg 또는 typing.GetMemberType을 사용하여 구현할 수 있습니다).

여기서 얻을 수 있는 잠재적인 이점은 타입 언어에 추가되는 새로운 구성 요소의 수를 줄이고, 연관 타입을 위한 새로운 일반 메커니즘을 도입하거나 Member에 대한 특수 처리를 마련해야 하는 일을 피할 수 있다는 것입니다.

PropsOnly (the query builder example에서 가져온 것)는 다음과 같은 형태입니다:

type PropsOnly[T] = typing.NewProtocol[
    *[
        typing.Member[typing.GetName[p], PointerArg[typing.GetType[p]]]
        for p in typing.Iter[typing.Attrs[T]]
        if typing.IsAssignable[typing.GetType[p], Property]
    ]
]

조건문과 반복에 타입 연산자를 사용하십시오.

다음과 같이 작성하는 대신:
  • tt if tb else tf
  • *[tres for T in Iter[ttuple]]
다음과 같은 타입 연산자 형식을 사용할 수 있습니다.
  • Cond[tb, tt, tf]
  • UnpackMap[ttuple, lambda T: tres]
  • 또는 T가 선언된 TypeVar여야 하는 UnpackMap[ttuple, T, tres]

불리언 연산도 마찬가지로 연산자가 됩니다(Not, And 등).

이렇게 하면 타입 어노테이션을 구성할 때 (점 표기법도 없앤다고 가정하면) 비자명한 계산을 수행할 필요가 전혀 없으므로, 타입 어노테이션의 평가를 지원하기 위한 런타임 훅이 필요하지 않다는 이점이 있습니다.

또한 원시 타입 어노테이션을 훨씬 쉽게 추출할 수 있습니다. (람다 형식은 여전히 다소 까다롭습니다. 람다가 아닌 형식은 추출하기가 간단하지만, TypeVar 선언을 요구하는 것은 최근 변경 사항의 방향과 맞지 않습니다.)

또 다른 이점은 특수한 <type-bool> 타입 클래스라는 개념이 필요하지 않다는 것입니다.

단점은 구문이 훨씬 더 나빠 보인다는 것입니다. 매핑하면서 필터링하는 것을 지원하면 더욱 나빠집니다(필터를 위한 추가 인자를 둘까요?).

필요하다면 다른 선택지도 살펴볼 수 있습니다.

일반 Python 함수로 타입 조작을 수행하십시오.

한 가지 제안은 타입 수준 조작을 위한 새로운 타입 언어 조각을 정의하는 대신, 일종의 “미니-mypy-플러그인” 역할을 하는 Python 함수(일부 집합)를 호출할 수 있도록 지원하자는 것입니다.

여기서 (저희가 보기에) 가장 큰 이점은 더 익숙한 실행 모델을 활용할 수 있다는 것입니다.

한 가지 제안된 이점은 이것이 제안을 단순화한다는 것이지만, 저희는 이 아이디어가 약속하는 단순화가 대부분 허상이며, 타입을 조작하기 위해 Python 함수를 호출하는 것이 오히려 훨씬 더 복잡하다고 생각합니다.

타입 검사기 내부에서 실행할 수 있도록, 잘 정의되고 안전하게 실행 가능한 언어(및 표준 라이브러리)의 하위 집합을 정의해야 합니다. 이와 같은 하위 집합은 다른 시스템에서 정의된 바 있습니다(Bazel의 구성 언어인 Starlark를 참조하십시오). 그러나 이는 여전히 고려해야 할 범위가 매우 넓으며, 프로그래머는 그 경계를 염두에 두어야 합니다.

또한 “미니-플러그인” 함수에서 타입이 어떻게 표현되는지에 대한 명확한 사양이 필요하며, 다양한 조작을 수행하기 위한 함수와 메서드도 정의해야 합니다. 이러한 함수는 이 PEP에서 현재 제안하는 내용과 상당히 많이 겹칠 것입니다.

런타임 사용이 필요하다면 타입 표현을 현재 typing이 작동하는 방식과 호환되도록 만들거나, 서로 다른 두 가지 런타임 타입 표현을 마련해야 합니다.

이것이 구문을 개선할지는 논쟁의 여지가 더 큽니다. 저희는 위에서 논의했지만 아직 주요 제안에 통합하지 않은 구문 정리 아이디어 중 일부를 채택하면 더 낮은 비용으로 구문을 개선할 수 있다고 생각합니다.

타입 수준 연산을 더 “엄격하게 타입 지정된” 형태로 만드십시오.

이 제안은 TypeScript보다 덜 “엄격하게 타입 지정되어 있습니다”(어쩌면 엄격한 종류 지정이라고 해야 할까요?).

TypeScript는 별칭 정의 위치에서 더 나은 타입 검사를 제공합니다. P[K]의 경우 Kkeyof P가 필요합니다. extends조건부 타입 연산자는 이를 지원할 수 있도록 타입을 좁힙니다.

TypeScript에서는 확장 시점에 실패하는 타입 별칭을 정의할 수 없지만, 이 시스템에서는 is실제로 가능합니다.

잠재적으로 이 역시 불가능하게 만들 수 있지만, 상당히 많은 추가 장치가 필요합니다.

  • KeyOf[T] - T의 리터럴 키입니다.
  • Member[T]는 타입 별칭을 정적으로 검사할 때 tuple[Member[KeyOf[T], object, str, ..., ...], ...]와 같은 어떤 타입을 가진 것으로 취급할 수 있습니다.
  • GetMemberType[T, S: KeyOf[T]] - 인덱스가 키여야 한다는 바운드를 요구하도록 GetMember에 바운드를 지정합니다. 하지만 이러한 종류의 종속 바운드는 현재 지원되지 않습니다. (TypeScript는 이를 지원합니다.)
  • 또한 컨텍스트에 민감한 타입 바운드 추론도 수행해야 합니다. 이는 미묘한 문제이지만, 분명히 이러한 종류의 작업은 항 수준에서 수행됩니다.

저희는 이것이 복잡성에 비해 가치가 없으며, 명백히 더 나은 방법이라고도 할 수 없다고 생각합니다. TypeScript에서는 흔히 여러 조건문을 작성해야 하는데, 그 조건문이 참 분기를 택하도록 의도된 경우가 많습니다. 일반적으로 거짓 분기는 never를 반환하며, 이러한 조건문은 추적하기가 상당히 어려울 수 있습니다.

잠재적인 향후 확장입니다.

Annotated 조작 지원입니다.

FastAPI와 같은 라이브러리는 어노테이션을 광범위하게 사용하며, 저희는 어노테이션을 사용하여 타입 수준의 계산 의사 결정을 수행할 수 있기를 바랍니다.

현재 Annotated는 타입 검사기에서 완전히 무시될 수 있으므로, 이를 검사하고 조작하는 기능을 지원하면 여러 어려움이 발생할 수 있다는 점에 유의하십시오.

이를 위한 잠재적인 API는 다음과 같을 수 있습니다.

  • GetAnnotations[T] - 잠재적으로 Annotated인 타입의 어노테이션을 리터럴로 가져옵니다. 예시입니다.:
    GetAnnotations[Annotated[int, 'xxx']] = Literal['xxx']
    GetAnnotations[Annotated[int, 'xxx', 5]] = Literal['xxx', 5]
    GetAnnotations[int] = Never
    
  • DropAnnotations[T] - 잠재적으로 Annotated인 타입의 어노테이션을 제거합니다. 예시입니다.:
    DropAnnotations[Annotated[int, 'xxx']] = int
    DropAnnotations[Annotated[int, 'xxx', 5]] = int
    DropAnnotations[int] = int
    

문자열 조작입니다.

TypeScript에는 문자열에 사용할 수 있는 “template literal” 타입이 있으며, 이를 통해 문자열 리터럴 타입을 연결하고 분해할 수 있습니다. 또한 대소문자 변환과 관련된 일련의 연산도 제공합니다.

연결을 지원하면 속성을 기반으로 새 메서드 이름을 생성하는 등의 사용 사례가 가능해집니다. 각 foo속성에 대해 get_foo 메서드를 생성할 수 있습니다.

슬라이싱을 지원하면 더 심층적인 문자열 순회를 수행할 수 있으며, 대소문자 변환을 지원하면 이름을 snake_case에서 CapitalizedWords로 변환하는 등의 연산이 가능해집니다.

실제로 여러 조건문을 사용하여 대소문자 변환 함수를 구현할 수 있지만, 그렇게 해서는 안 됩니다. 특히 모든 유니코드에서 작동하도록 하려는 경우에는 더욱 그렇습니다.

슬라이싱과 연결만 지원하는 것도 분명히 가능할 것입니다.

  • Slice[S: Literal[str], Start: Literal[int | None], End: Literal[int | None]]: 문자열 타입의 슬라이싱도 지원합니다. (현재는 튜플이 지원됩니다.)
  • Concat[S1: Literal[str], S2: Literal[str]]: 두 문자열을 연결합니다.
  • Uppercase[S: Literal[str]]: 문자열 리터럴을 대문자로 변환합니다.
  • Lowercase[S: Literal[str]]: 문자열 리터럴을 소문자로 변환합니다.
  • Capitalize[S: Literal[str]]: 문자열 리터럴의 첫 글자를 대문자로 변환합니다.
  • Uncapitalize[S: Literal[str]]: 문자열 리터럴의 첫 글자를 소문자로 변환합니다.

이 절의 모든 연산자는 유니언 타입 위로 리프트됩니다.

베이스를 사용하는 새 프로토콜

때때로 NewProtocolWithBases와 같은 것을 다음과 같은 사양으로 지원하는 것이 유용할 수 있습니다.

  • NewProtocolWithBases[Bases: tuple[type], *Ms: Member]

주어진 모든 베이스를 확장하고 지정된 멤버를 가지는 타입은 이 프로토콜을 만족한다는 것이 이 아이디어의 핵심입니다.

이는 새 Pydantic 모델을 생성하는 것과 같은 작업을 수행하려는 상황에서 유용합니다.

베이스를 사용하는 프로토콜은 프로토콜이 될 수 있는 것에 대한 추가 사항이며 아직 다루고 싶지 않은 내용이고, 많은 사용 사례를 다른 방식으로 시뮬레이션할 수 있기 때문에 현재로서는 이를 완전한 제안으로 제시하는 것을 보류합니다.

미해결 문제

  • Unpack of typevars for **kwargs: 추가 인자에 대해 리터럴 타입을 추론하려고 하는지 여부를 바운드 역할을 하는 TypedDict에서 어떤 방식으로든 구성할 수 있어야 합니까? readonlyTypedDict의 매개변수로 추가했다면 이를 사용했겠지만, 그렇게 하지 않았습니다.
  • Members: Members는 어노테이션이 없는 메서드를 포함하여 모든 메서드를 반환해야 합니까? 속성과 어느 정도 일관성을 유지하려는 의도에서 이들을 제외했지만, 정적 평가기나 런타임 평가기에 이들을 포함하는 것은 기술적으로 어렵지 않습니다.
  • 제네릭 호출 가능 객체: GenericCallable을 검사하거나 분해할 수 있는 메커니즘을 마련해야 합니까? 변수 정보를 가져와 구체적인 타입에 적용할 수도 있을 것입니다.
  • 클래스 업데이트: UpdateClass는 타입 평가 순서 의존성을 도입합니다. 어떤 __init_subclass__UpdateClass반환 타입이 관련 없는 다른 클래스의 Members를 검사하고, 해당 클래스에도 __init_subclass__가 있다면 결과는 이들을 어떤 순서로 평가하는지에 따라 달라질 수 있습니다. 이상적으로는 이러한 경우를 거부해야 합니다. 다만 이는 잠재적인 런타임 평가 순서 의존성을 실제로 정확히 반영합니다.
  • RaiseError는 타입을 출력할 때 문자열 템플릿을 지원해야 합니까?
  • 제네릭 함수 때문에 타입 연산자를 평가할 수 없는 경우가 많이 발생합니다. 이는 해결되지 않은 타입 변수에 적용되기 때문이며, 그러한 경우에 타입 평가 규칙이 정확히 어떠해야 하는지는 다소 불분명합니다.

    현재 mypy의 개념 증명 구현에서는 평가가 중단된 타입에 대해 서브타입 검사를 완전히 불변 방식으로 수행합니다. 연산자가 일치하는지, 그리고 두 인자에서 모든 피연산자가 불변 방식으로 일치하는지를 검사합니다.

감사의 말

설계에 관해 많은 논의를 나눈 Jukka Lehtosalo에게 감사의 말씀을 전하고 싶습니다.

또한 이 제안에 상당한 영향을 준 TypeScript 팀의 언어에도 감사의 말씀을 전하고 싶습니다!

각주

  • Hans Boehm의 “Partial polymorphic type inference is undecidable”: https://dl.acm.org/doi/10.1109/SFCS.1985.44
  • Frank Pfenning의 “On the Undecidability of Partial Polymorphic Type Reconstruction”: https://www.cs.cmu.edu/~fp/papers/CMU-CS-92-105.pdf

    다만 이 환경에서는 함수의 제네릭 타입을 추론하려고 하지 않으므로, 일부 문제를 피할 수 있을지도 모릅니다. 반면에 서브타이핑이 있습니다. (솔직히 말해 이러한 문제 중 일부에는 이미 상당히 깊이 관여하고 있습니다.)