본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
Type system; 타입 시스템, 형 체계
프로그램의 값과 식에 타입을 붙이고, 타입 규칙에 어긋나는 연산을 찾아내는 규칙의 체계

타입 시스템(type system)은 프로그래밍 언어에서 값, 변수, 식, 함수 같은 구성 요소에 타입(자료형)을 붙이고, 타입 사이의 관계와 각 타입에 허용되는 연산을 정한 규칙의 체계다. 벤저민 피어스는 이를 "프로그램의 구절을 그것이 계산하는 값의 종류에 따라 분류해, 어떤 동작이 일어나지 않음을 증명하는 다루기 쉬운 구문적 방법"이라고 정의했다.[1] 타입 시스템을 수학적으로 연구하는 분야가 타입 이론(type theory)이다.

컴퓨터 하드웨어는 메모리의 비트열이 정수인지, 부동소수점 수인지, 문자인지, 주소인지 구별하지 않는다. 타입은 이 비트열에 의미를 붙인다. 타입 시스템의 첫째 목적은 의미 없는 연산, 예를 들어 3 / "Hello"처럼 정수를 문자열로 나누는 연산을 찾아내 막는 것이다. 이런 불일치를 타입 오류라고 한다. 그 밖에 코드 부품 사이의 인터페이스를 정하고 연결이 맞는지 검사하며(모듈화), 함수가 무엇을 받고 돌려주는지 적어 두는 문서 역할을 하고, 컴파일러가 알맞은 기계 명령과 메모리 배치를 고르게 하며(최적화), IDE의 자동 완성, 정의로 이동, 안전한 이름 바꾸기 같은 도구 기능의 바탕이 된다.

모든 프로그래밍 언어에는 타입이 있다. "동적 타입 언어에는 타입이 없다"는 말은 오해다. Python에서 12는 int, "Hello"는 str 타입을 가지며, print가 둘을 다르게 출력할 수 있는 것도 실행 중에 각 값이 타입 정보를 지니기 때문이다. 언어마다 다른 것은 타입을 언제 검사하는지, 규칙이 얼마나 엄격한지, 타입끼리 어떤 관계로 호환되는지다.

타입 검사: 정적과 동적

편집 원본 편집

타입 규칙을 지키는지 확인하는 과정을 타입 검사(type checking)라 한다. 실행 전에 소스 코드를 분석해 검사하면 정적 타입 검사, 실행 중에 값을 보며 검사하면 동적 타입 검사다. 각각의 특징과 장단점은 정적 타입 언어와 동적 타입 언어 문서에서 자세히 다룬다.

항목 정적 타입 검사 동적 타입 검사
검사 시점 컴파일할 때(또는 실행 전 분석 도구) 연산을 실행하는 순간
타입이 붙는 대상 변수와 식 값(런타임 객체에 타입 꼬리표가 붙음)
오류 발견 실행하지 않은 코드 경로의 오류도 찾음 그 경로를 실제로 실행해야 드러남
성능 검사를 컴파일 때 끝내 실행 중 검사가 줄고 최적화가 쉬움 실행 중 타입 확인 비용, JIT가 추측으로 보완
유연성 일부 올바른 프로그램도 거부될 수 있음 eval, 목(mock) 객체, 메타프로그래밍이 쉬움
대표 언어 C, C++, Java, C#, Go, Rust, Haskell, Swift, Kotlin Python, JavaScript, Ruby, PHP, Lua, Lisp 계열

정적 검사는 모든 입력에 대해 성립하는 성질을 보장하는 일종의 가벼운 프로그램 검증이다. 테스트 커버리지 100%로도 찾기 어려운, 드물게 실행되는 경로의 타입 오류를 잡아낸다. 대신 튜링 완전한 언어에서 정적 검사는 필연적으로 보수적이다. 건전하면서(잘못된 프로그램을 모두 거부) 결정 가능한(검사가 반드시 끝나는) 타입 시스템은 불완전할 수밖에 없어, 실제로는 오류가 나지 않는 프로그램도 일부 거부한다.[1] 예를 들어 if 복잡한_조건 then 정상_처리 else 타입이_안_맞는_식에서 조건이 늘 참이라도 대부분의 타입 검사기는 else 쪽 때문에 프로그램을 거부한다.

정적 검사만으로 확인할 수 없는 기능도 있어, 정적 타입 언어도 대부분 동적 검사를 함께 한다. 상위 타입의 값을 하위 타입으로 바꾸는 다운캐스트가 대표적이다. Java의 (Dog) animal은 실행 중에 실제 객체가 Dog인지 확인하고 아니면 ClassCastException을 던진다. 이를 위해 객체에 남겨 두는 타입 정보를 런타임 타입 정보(RTTI)라 하며, 동적 디스패치, 리플렉션도 이것을 쓴다. 반대로 정적 검사를 일부러 끄는 장치도 있다. C#은 4.0부터 dynamic 타입을 두어 해당 변수를 실행 시점에 검사하고, Rust는 dyn Any로 동적 타입 확인을 한다.

정적 타입과 동적 타입의 차이는 "타입이 언제 확정되고 검사되는가"뿐이다. 동적 타입 언어인 Python도 값마다 고정된 타입이 있고, 마음대로 다른 타입으로 바뀌지 않는다. 한편 Python과 JavaScript 진영이 타입 힌트와 TypeScript로 정적 검사를 받아들이는 흐름은 대규모 코드에서 도구 지원과 유지 보수의 이점이 크기 때문이다.

강한 타입과 약한 타입

편집 원본 편집

강한 타입(strongly typed)과 약한 타입(weakly typed)은 널리 쓰이지만 합의된 정의가 없는 용어다. 대체로 서로 다른 타입 사이의 암묵적 변환(형 변환, coercion)을 얼마나 허용하는지, 타입 규칙을 우회하는 수단이 얼마나 쉬운지를 가리킨다. 정적·동적 구분과는 독립된 축이다.

"5" - 1      // 4    (문자열을 숫자로 암묵 변환)
"5" + 1      // "51" (숫자를 문자열로 암묵 변환)
[] + {}      // "[object Object]"
"5" + 1      # TypeError: can only concatenate str (not "int") to str
int("5") + 1 # 6 (명시적 변환)
암묵 변환이 적음(흔히 강한 타입) 암묵 변환이 많음(흔히 약한 타입)
정적 Haskell, Rust, OCaml, Java(대체로), Go C(정수와 부동소수점, 포인터와 정수 사이 변환, 같은 크기면 아무 타입으로나 캐스트)
동적 Python, Ruby JavaScript, PHP, Perl

같은 언어를 두고도 기준에 따라 평가가 갈린다. C는 정수 크기와 부호, 정수와 부동소수점 사이의 변환을 대부분 알아서 해 주고, 캐스트로 아무 메모리나 다른 타입으로 해석할 수 있어 약한 타입 쪽으로 분류되는 경우가 많다. Java는 암묵 변환이 적지만 "a" + 1처럼 문자열 연결에서는 숫자를 문자열로 바꾼다. 그래서 논의할 때는 강·약 대신 "암묵적 변환 규칙", "타입 안전성", "메모리 안전성"처럼 구체적인 말을 쓰는 편이 정확하다. 흔한 오해와 달리 형 변환을 명시해야 하느냐는 정적·동적의 문제가 아니며, 정적 타입 언어라고 모든 변수에 타입을 적어야 하는 것도 아니다(아래 타입 추론 참고).

타입 안전성과 건전성

편집 원본 편집

타입 안전(type-safe) 언어는 타입 시스템의 규칙을 어기는 연산이나 변환을 허용하지 않는 언어다. 메모리 안전(memory-safe) 언어는 할당받지 않은 메모리에 접근하지 못하게 하는 언어로, 예를 들어 배열 범위를 검사한다. 둘은 대개 함께 간다. 포인터 연산과 정수에서 포인터로의 변환을 허용하는 C는 둘 다 아니다.

int x = 5;
char y[] = "37";
char *z = x + y;     /* 문자열 끝을 넘어선 주소 */
printf("%c\n", *z);  /* 정의되지 않은 동작 */

타입 시스템이 건전(sound)하다는 것은 타입 검사를 통과한 프로그램이 실행 중에 타입 오류를 일으키지 않는다는 뜻이다. 로빈 밀너는 1978년 논문에서 이를 "타입이 올바른 프로그램은 잘못될 수 없다(well-typed programs cannot go wrong)"는 의미론적 건전성 정리로 증명했다.[2] 여기서 "잘못됨"은 타입 오류를 말할 뿐이며, 0으로 나누기나 배열 범위 초과처럼 타입 시스템이 표현하지 않는 오류는 여전히 실행 중에 난다.

실용 언어의 타입 시스템은 일부러 또는 설계 결과로 건전하지 않은 경우가 많다.

  • TypeScript는 JavaScript의 흔한 패턴을 받아들이려고 함수 매개변수의 양변성 같은 불건전한 규칙을 의도적으로 허용한다. strictFunctionTypes 옵션으로 일부를 조일 수 있다.[3] any와 타입 단언도 검사를 우회한다.
  • Java는 배열을 공변으로 취급해(String[]를 Object[]로 쓸 수 있음) 잘못된 저장을 컴파일 때 막지 못하고 실행 중 ArrayStoreException으로 잡는다.
  • 2016년 논문은 null과 와일드카드 타입의 조합으로 캐스트 없이 아무 타입을 아무 타입으로 바꾸는 짧은 프로그램을 만들어 Java와 Scala의 타입 시스템이 불건전함을 보였다.[4]

명목적 타입과 구조적 타입

편집 원본 편집

두 타입이 호환되는지 판단하는 기준도 언어마다 다르다.

  • 명목적 타입(nominal typing): 타입의 이름과 선언된 상속·구현 관계로 판단한다. class Dog implements Pet처럼 명시해야 Dog를 Pet 자리에 쓸 수 있다. Java, C#, C++, Swift, Rust의 트레이트가 이 방식이다.
  • 구조적 타입(structural typing): 타입이 가진 멤버의 구조로 판단한다. 필요한 멤버를 모두 갖추면 이름이나 선언과 상관없이 호환된다. TypeScript, OCaml의 객체 타입, Go의 인터페이스가 이 방식이다.[3]
  • 덕 타이핑(duck typing): "오리처럼 걷고 오리처럼 꽥꽥거리면 오리"라는 말처럼, 실행 중에 객체가 필요한 메서드를 실제로 가지고 있으면 그대로 쓴다. Python, Ruby, JavaScript에서 흔하다. 구조적 타입을 실행 시점에 확인하는 셈이다.

덕 타이핑의 편리함은 동적 타입 언어만의 것이 아니다. 구조적 타입을 쓰는 정적 타입 언어는 타입 사이의 관계를 선언하지 않아도 컴파일 시점에 같은 검사를 끝내므로, 필요한 메서드가 없는 값을 넘기면 실행 전에 오류를 낸다. 이를 정적 덕 타이핑이라고도 한다. Python도 PEP 544의 Protocol로 타입 검사기에 구조적 타입을 알려 줄 수 있다.

type Speaker interface { Speak() string }

type Duck struct{}
func (Duck) Speak() string { return "꽥" }   // implements 선언이 없다

func talk(s Speaker) { fmt.Println(s.Speak()) }

func main() { talk(Duck{}) }                // Duck은 Speak()를 가졌으므로 Speaker로 통과

C나 옛 Java처럼 변수마다 타입을 적어야 하는 언어도 있지만, 많은 정적 타입 언어는 컴파일러가 코드의 쓰임새를 보고 타입을 알아낸다. 이를 타입 추론(type inference)이라 한다.

가장 강력한 체계는 힌들리-밀너(Hindley-Milner) 타입 시스템이다. 로저 힌들리가 1969년 조합 논리에서 모든 타입을 대입으로 얻을 수 있는 가장 일반적인 타입인 주 타입 스킴(principal type-scheme)이 존재하고 계산 가능함을 보였고,[5] 밀너가 1978년 ML 언어를 위해 독립적으로 같은 결과와 추론 알고리즘을 내놓았으며,[2] 1982년 다마스와 밀너가 알고리즘 W의 완전성을 증명했다.[6] 이 체계에서는 타입 표기가 하나도 없어도 프로그램 전체의 가장 일반적인 타입을 찾는다. ML, OCaml, F#, Haskell이 이를 바탕으로 한다.

-- 타입을 적지 않아도 GHCi는 가장 일반적인 타입을 추론한다
-- (타입 변수 이름은 GHC가 붙이는 대로 달라질 수 있다)
compose f g x = f (g x)
-- ghci> :t compose
-- compose :: (b -> c) -> (a -> b) -> a -> c

average xs = sum xs / fromIntegral (length xs)
-- ghci> :t average
-- average :: (Fractional a, Foldable t) => t a -> a

타입 추론은 암묵적 형 변환과 다르다. 추론은 각 식에 맞는 타입 하나를 찾아 주는 것이고, 값을 다른 타입으로 바꾸지 않는다. Haskell에는 정수와 실수 사이의 암묵 변환이 없어서 위 예처럼 fromIntegral로 명시적으로 바꿔야 하며, 같은 변수를 호환되지 않는 두 타입으로 쓰면 변환해 주는 대신 컴파일 오류를 낸다. 다만 숫자 리터럴 자체가 여러 숫자 타입이 될 수 있는 다형 값이어서, 겉보기에는 변환이 일어나는 것처럼 보인다. 힌들리-밀너 체계는 표현력에 한계가 있어, 더 높은 계층의 다형성(rank-N)을 허용하면 추론이 일반적으로 불가능해지고 일부 타입 표기가 필요하다.

주류 언어는 대개 함수 안의 지역 변수만 추론하는 지역 타입 추론을 쓴다. C++11의 auto, C# 3.0의 var, Java 10의 var(JEP 286),[7] Go의 :=, Kotlin·Swift·Scala·Rust·TypeScript의 변수 선언이 그렇다. 동적 타입 언어에서도 정적 분석기(Pyright 등)와 JIT 컴파일러가 코드나 실행 정보를 보고 타입을 추정하지만, 이는 언어의 타입 검사가 아니라 도구의 분석이다.

점진적 타이핑

편집 원본 편집

점진적 타이핑(gradual typing)은 한 언어 안에서 타입 표기가 있는 부분은 정적으로, 없는 부분은 동적으로 검사하는 방식이다. 제러미 시크와 왈리드 타하가 2006년 체계화했다.[8] 정적으로 알 수 없는 타입을 나타내는 특별한 타입(any, Any, dynamic)을 두고, 타입 같음 대신 "일관성(consistency)" 관계로 호환성을 판단한다. 동적 타입은 모든 타입과 일관되지만, 이 관계는 추이적이지 않다(int와 any, any와 string이 각각 일관되어도 int와 string은 일관되지 않다).

  • TypeScript: 2012년 10월 마이크로소프트가 공개한 JavaScript의 상위 집합이다.[9] 타입 검사 뒤 타입 표기를 지운 JavaScript를 내놓으므로 실행 중에는 타입 검사가 없다.
  • Python 타입 힌트: PEP 484(Python 3.5)로 표준화되었다. 힌트는 __annotations__에 저장될 뿐 실행 중에 검사하지 않으며, mypy, Pyright 같은 외부 도구가 검사한다. PEP는 "Python은 동적 타입 언어로 남을 것이며 타입 힌트를 의무화할 생각이 없다"고 명시했다.[10]
  • PHP의 타입 선언과 파일 단위 declare(strict_types=1), Dart, Typed Racket, Clojure와 Common Lisp의 선택적 타입 선언(최적화용)도 비슷한 흐름이다.

엄밀히는 선택적 타이핑(optional typing)과 구별하기도 한다. TypeScript와 Python처럼 정적 분석에만 타입을 쓰고 실행 중에는 강제하지 않는 것이 선택적 타이핑이고, 정적 부분과 동적 부분의 경계에서 실행 중 검사를 넣어 타입 안전성을 지키는 것이 좁은 의미의 점진적 타이핑이다.

def greet(name: str) -> str:
    return "hello, " + name

greet(42)   # 실행은 되다가 + 에서 TypeError, mypy는 실행 전에 인자 타입 오류를 보고

다형성(polymorphism)은 하나의 코드가 여러 타입의 값에 동작하는 성질이다. 크리스토퍼 스트레이치가 1967년 강의록에서 매개변수 다형성과 임시(ad hoc) 다형성을 구별했고,[11] 루카 카르델리와 피터 웨그너가 1985년 이를 넓혀 분류했다.[12]

종류 뜻 예
매개변수 다형성(parametric) 타입을 매개변수로 받아 모든 타입에 똑같이 동작 Java·C#의 제네릭 List<T>, Haskell의 length :: [a] -> Int
서브타입 다형성(subtype, inclusion) 하위 타입의 값을 상위 타입 자리에 쓸 수 있음. 실제 동작은 동적 디스패치로 결정 객체 지향 언어의 상속과 인터페이스, 리스코프 치환 원칙
임시 다형성: 오버로딩(overloading) 같은 이름의 함수를 타입마다 따로 정의 C++·Java의 메서드 오버로딩, 연산자 오버로딩
임시 다형성: 타입 클래스·트레이트 타입별 구현을 규칙적으로 묶어 제약으로 사용 Haskell의 Num a =>, Rust의 트레이트 경계 T: Display
강제 변환(coercion) 값을 기대하는 타입으로 자동 변환해 같은 연산에 통과 1 + 2.0에서 정수를 실수로 승격

제네릭(generic)은 매개변수 다형성을 구현한 기능이다. 언어마다 구현 전략이 다르고, 그 차이가 실행 중 동작에 드러난다.

전략 방식 예 영향
타입 소거(erasure) 컴파일 후 타입 매개변수를 경계 타입이나 Object로 바꾸고 필요한 곳에 캐스트를 넣음 Java(JDK 5, 2004)[13], TypeScript 실행 중 List<String>과 List<Integer>를 구별하지 못함, 기본형을 직접 못 씀
실체화(reification) 실행 시점에도 타입 인자 정보를 유지 C#·.NET(2.0부터) 실행 중 타입 확인 가능, 값 타입에 특화된 코드 생성
단형화(monomorphization) 쓰인 타입 인자마다 코드를 복제해 따로 컴파일 C++ 템플릿, Rust 실행이 빠르지만 바이너리가 커지고 컴파일이 느림
사전 전달·혼합 타입별 연산표를 숨은 인자로 넘기거나, 메모리 모양이 같은 타입끼리 코드를 공유 Haskell 타입 클래스, Go(1.18부터)[14] 코드 크기와 속도의 절충

제네릭 타입 사이의 하위 타입 관계를 변성(variance)이라 한다. Cat이 Animal의 하위 타입일 때 List<Cat>도 List<Animal>의 하위 타입이면 공변(covariant), 반대 방향이면 반공변(contravariant), 둘 다 아니면 무공변(invariant)이다. 읽기만 하는 자리는 공변, 쓰기만 하는 자리는 반공변이어야 안전하다. Java는 사용하는 곳에서 ? extends T, ? super T로, C#과 Kotlin은 선언하는 곳에서 out, in으로 지정한다.

대수적 자료형

편집 원본 편집

대수적 자료형(algebraic data type)은 곱 타입과 합 타입을 조합해 만든 타입이다.

  • 곱 타입(product type): 여러 값을 동시에 가진다. 구조체, 레코드, 튜플이 여기에 속한다. 가능한 값의 수가 각 필드 값의 수의 곱이다.
  • 합 타입(sum type): 여러 경우 가운데 정확히 하나다. 태그된 공용체(tagged union), 판별 공용체라고도 한다. 가능한 값의 수가 각 경우의 합이다.

합 타입은 패턴 매칭과 함께 쓰여, 컴파일러가 모든 경우를 처리했는지(완전성, exhaustiveness) 검사해 준다. null 대신 "값이 있거나 없음"을 나타내는 Option, Maybe, "성공 또는 오류"를 나타내는 Result, Either를 타입으로 표현해 널 참조 오류를 타입 수준에서 막는다.

enum Shape {
    Circle { r: f64 },
    Rect { w: f64, h: f64 },
}

fn area(s: &Shape) -> f64 {
    match s {                       // 경우를 하나라도 빼면 컴파일 오류
        Shape::Circle { r } => 3.14159 * r * r,
        Shape::Rect { w, h } => w * h,
    }
}

Haskell, OCaml, F#, Rust, Swift, Scala, Kotlin(sealed 클래스)이 합 타입을 직접 지원하고, TypeScript는 공통 판별 필드를 둔 유니언 타입으로, Java는 17의 sealed 클래스(JEP 409)와 21의 switch 패턴 매칭(JEP 441)으로 같은 표현을 할 수 있다.[15] C의 union은 어느 경우인지 기록하지 않아 타입 안전하지 않다.

유니언 타입과 교차 타입도 비슷한 개념이다. TypeScript의 A | B는 A 또는 B인 값으로, 두 타입에 공통인 연산만 할 수 있다. A & B는 A이면서 B인 값으로, 두 타입의 멤버를 모두 가진다. 교차 타입은 오버로딩된 함수의 타입을 표현하는 데도 쓰인다.

고급 타입 개념

편집 원본 편집
개념 설명 언어·연구
의존 타입(dependent type) 타입이 값에 의존한다. 예: 길이가 n인 벡터 Vec n, 행렬 곱을 matrix(k,m) × matrix(m,n) → matrix(k,n)으로 표현해 크기 불일치를 컴파일 때 거부. 타입 검사가 일반적으로 결정 불가능해져 증명을 직접 써야 할 때가 많다 Agda, Idris, Coq(Rocq), Lean, Dependent ML
선형·아핀 타입(linear, affine) 값을 정확히 한 번(선형) 또는 많아야 한 번(아핀) 쓰도록 강제. 자원 해제와 제자리 수정을 안전하게 한다 Rust의 소유권과 이동(아핀에 가까움), Linear Haskell, Clean의 유일성 타입
정제 타입(refinement type) 기존 타입에 논리 조건을 붙인다. 예: 0이 아닌 정수 Liquid Haskell, F*
존재 타입(existential type) "어떤 타입 X가 있어서"를 표현해 구현을 숨기고 인터페이스만 드러낸다. 추상 자료형과 모듈의 이론적 기초 1988년 Mitchell과 Plotkin의 "추상 타입은 존재 타입이다", Haskell의 존재 양화, Rust의 impl Trait
고계 타입(higher-kinded type) 타입 생성자(List, Option)를 타입 매개변수로 받는다 Haskell의 Functor f, Scala
효과 시스템(effect system) 함수가 일으키는 부작용(예외, 입출력, 상태 변경)을 타입에 기록 Java의 검사 예외(제한적 형태), Koka, OCaml 5의 효과 처리기

타입 시스템이 표현력이 커질수록 더 많은 오류를 컴파일 때 잡지만, 타입 추론이나 타입 검사 자체가 결정 불가능해지고 프로그래머가 적어야 할 표기가 늘어난다. 표현력과 사용성 사이의 이 긴장이 타입 시스템 연구의 주제다.

통합 타입 시스템

편집 원본 편집

C#과 Scala는 기본형을 포함한 모든 타입이 하나의 최상위 타입(C#은 object, Scala는 Any)에서 파생되는 통합 타입 시스템을 쓴다. Java는 최상위 클래스 Object가 있지만 int, double 같은 기본형은 객체가 아니어서, Integer 같은 래퍼 클래스와 자동 박싱으로 둘을 오간다.

언어 검사 시점 암묵 변환 호환성 기준 추론 비고
C 정적 많음 명목적(구조체 이름) 거의 없음(C23 auto) 캐스트로 타입 우회 가능, 메모리 안전 아님
C++ 정적 중간(사용자 정의 변환 포함) 명목적, 템플릿은 구조적 지역(auto) 템플릿 단형화, C++20 콘셉트
Java 정적 + 동적 검사 적음 명목적 지역(var, Java 10) 제네릭 타입 소거, 배열 공변
C# 정적(+ dynamic) 적음 명목적 지역(var) 실체화된 제네릭, 통합 타입
Go 정적 거의 없음 인터페이스는 구조적 지역(:=) 1.18부터 제네릭
Rust 정적 거의 없음 명목적(트레이트) 함수 안 전체 소유권, 대수적 자료형
Haskell, OCaml, F# 정적 없음 명목적(OCaml 객체는 구조적) 힌들리-밀너 전역 추론 대수적 자료형, 타입 클래스(Haskell)
Scala, Kotlin, Swift 정적 적음 명목적 지역 널 가능 타입 구분(Kotlin, Swift)
TypeScript 정적(선택적) JavaScript와 같음 구조적 강함 의도적으로 불건전, 실행 중 검사 없음
Python 동적(+ 타입 힌트) 적음 덕 타이핑, 힌트는 Protocol로 구조적 도구가 추론 값마다 고정 타입
Ruby 동적 적음 덕 타이핑 없음(RBS, Sorbet은 별도 도구)
JavaScript 동적 많음 덕 타이핑 없음 ==의 변환 규칙이 복잡해 === 권장
PHP 동적(+ 타입 선언) 많음(strict_types로 줄임) 명목적(클래스), 덕 타이핑 없음
Lisp, Clojure 동적 적음 덕 타이핑 없음 최적화용 선택적 타입 선언
Erlang, Elixir 동적 적음 패턴 매칭 없음 Dialyzer로 정적 분석