Перейти к содержанию

Вывод типов

Чтобы не повторять определения, которые были даны в главе Связывание, типизация, вывод типов, эту главу стоит начать с неформально определения.

Вывод типов — это механизм, благодаря которому разрабатывать на статически типизированном TypeScript становится намного легче. Он берет на себя ту работу, которая во многих случаях может показаться рутинной. Вывод типов работает не только тогда, когда вы не указывает тип явно; он работает всегда, когда компилятор встречает код. Дело в том, что компилятор не доверяет разработчику и поэтому весь код, в штатном режиме, проходит через вывод типов. В этом механизме нет ничего сложного, но несмотря на это, у разработчиков, которые только приступают к изучению TypeScript, некоторые неочевидные особенности могут вызвать вопросы, на которые и постарается ответить данная глава.

Кроме того, ещё одна цель этой главы — раскрыть те моменты, рассказ о которых в других частях книги мог быть не ко времени и излишне усложнил бы понимание. Поэтому те части книги просто ссылались на текущую главу. Но, также в этой главе вкратце будет повторено то, что уже было рассмотрено по ходу изложения материала.

Вывод примитивных типов

Вывод типов для значений, принадлежащих к так называемым примитивным типам, не таит в себе ничего необычно. Кроме того, будь это переменные, поля, параметры, возвращаемые из функций и методов значения, результат во всех случаях будет идентичным.

enum Enums {
  Value,
}

let v0 = 0 // let v0: number
let v1 = 'text' // let v1: string
let v2 = true // let v2: boolean
let v3 = Symbol() // let v3: symbol
let v4 = Enums.Value // let v4: Enums

Вывод примитивных типов для констант (const) и полей только для чтения (readonly)

Когда дело доходит до присваивания значений, принадлежащих к примитивным типам, таким конструкциям, как константы (const) и неизменяемые поля (модификатор readonly), поведение вывода типов изменяется.

В случае, когда значение принадлежит к примитивным типам number, string или boolean, вывод типов указывает принадлежность к литеральным примитивным типам, определяемым самим значением.

enum Enums {
  Value,
}

const v0 = 0 // let v0: 0
const v1 = 'text' // let v1: 'text'
const v2 = true // let v2: true

class Identifier {
  readonly f0 = 0 // f0: 0
  readonly f1 = 'text' // f1: 'text'
  readonly f2 = true // f2: true
}

Если значение принадлежит к типу enum, то вывод типов установит принадлежность к типу enum.

enum Enums {
  Value,
}

const v = Enums.Value // let v: Enums

class Identifier {
  readonly f = Enums.Value // f: Enums
}

Когда вывод типов встречает значение, принадлежащие к типу symbol, его поведение зависит от конструкции, которой присваивается значение. Так, если вывод типов работает с константой, то тип определяется, как запрос типа (глава Type Queries (запросы типа), Alias (псевдонимы типа)) самой константы. Если же вывод типов устанавливает принадлежность к типу-неизменяемому полю, то тип будет определен, как symbol. Происходит так потому, что при создании каждого нового экземпляра в системе будет определяться и новый символ, что противоречит правилам, установленным для Unique Symbol (глава Примитивные литеральные типы Number, String, Boolean, Unique Symbol, Enum).

const v = Symbol() // const v: typeof v

class Identifier {
  readonly f = Symbol() // f: symbol
}

Вывод объектных типов

С выводом объектных типов не связанно ничего необычного. Кроме того, поведение вывода типов одинаково для всех конструкций.

class ClassType {}
interface InterfaceType {}

type TypeAlias = number

let typeIntreface: InterfaceType
let typeTypeAlias: TypeAlias

let v0 = { a: 5, b: 'text', c: true } // let v0: { a:number, b:string, c: boolean }
const v1 = { a: 5, b: 'text', c: true } // let v1: { a:number, b:string, c: boolean }

let v3 = new ClassType() // let v3: ClassType
let v4 = typeIntreface // let v4: InterfaceType
let v5 = typeTypeAlias // let v5: number

Вывод объединенных (Union) типов

С выводом типов объединения (глава “Типы - Union, Intersection”) связаны как очевидные, так и нет, случаи.

К очевидным случаям можно отнести массив, состоящий из разных примитивных типов. В таком случае будет выведен очевидный тип объединение, который составляют типы присутствующих в массиве примитивов.

let v = [0, 'text', true] // let v: (string | number | boolean)[]

В случае получения любого элемента массива, вывод типов также установит принадлежность к объединенному типу.

let v = [0, 'text', true] // let v: (string | number | boolean)[]

let item = v[0] // let item: string | number | boolean

Неочевидные особенности лучше всего начать с примера, в котором вывод типа определяет принадлежность к массиву обычных объектных типов.

let v = [
  { a: 5, b: 'text' },
  { a: 6, b: 'text' },
] // let v: { a: number, b: string }[]

В примере, вывод типов выводит ожидаемый и предсказуемый результат для массива объектов, чьи типы полностью идентичны. Идентичны они по той причине, что вывод типов установит тип { a: number, b:string } для всех элементов массива.

Но стоит изменить условие, допустим, убрать объявление одного поля и картина кардинально изменится. Вместо массива обычных объектов, тип будет выведен, как массив объединенного типа.

let v = [{ a: 5, b: 'text' }, { a: 6 }, { a: 7, b: true }] // let v: ({ a: number, b: string } | { a: number, b?: undefined } | { a: number, b: boolean })[]

Как видно из примера выше, вывод типов приводит все объектные типы, составляющие тип объединение, к единому виду. Он добавляет к типам не существующие в них, но существующие в других объектных типах, поля, декларируя их как необязательные (глава Операторы - Optional, Not-Null, Not-Undefined, Definite Assignment Assertion). Сделано это для того, чтобы можно было конкретизировать тип любого элемента массива. Простыми словами, чтобы не получить ошибку во время выполнения, любой элемент массива должен иметь общие для всех элементов признаки. Но так как в реальности в объектах некоторые члены вовсе могут отсутствовать, вывод типов, чтобы повысить типобезопасность, декларирует их, как необязательные. Таким образом, он предупреждает разработчика о возможности возникновения ситуации, при которой эти члены будут иметь значение undefined, что и демонстрируется в примере ниже.

let v = [{ a: 5, b: 'text' }, { a: 6 }, { a: 7, b: true }] // let v: ({ a: number, b: string } | { a: number, b?: undefined } | { a: number, b: boolean })[]

let a = v[0].a // let a: number
let b = v[0].b // let b: string | boolean | undefined

Если в качестве значений элементов массива выступают экземпляры классов, не связанных отношением наследования, то объединенные типы будут составлять их типы.

class A {
  public a: number = 0
}

class B {
  public a: string = ''
  public b: number = 5
}

let v = [new A(), new B()] // let v: (A | B)[]

В случае, если элементы массива являются экземплярами классов, связанных отношением наследования (номинативная типизация Совместимость типов на основе вида типизации), то выводимый тип будет ограничен максимально базовым из типом участвующем в операции выводе.

class A {}
class B extends A {
  f0 = 0
}
class C extends A {
  f1 = ''
}
class D extends A {
  f2 = true
}
class E extends D {
  f3 = {}
}

let v3 = [new A(), new B(), new C(), new D(), new E()] // let v3: A[]
let v4 = [new B(), new C(), new D(), new E()] // let v4: (B | C | D)[]

Те же самые правила применяются для вывода типа, возвращаемого тернарнарным оператором.

class A {}
class B extends A {
  f0 = 0
}
class C extends A {
  f1 = ''
}
class D extends A {
  f2 = true
}
class E extends D {
  f3 = {}
}

let v0 = false ? new A() : new B() // let v0: A
let v1 = false ? new B() : new C() // let v1: B | C
let v2 = false ? new C() : new D() // let v2: C | D

Так как выражение расположенное в блоке тернарного оператора вычисляется на этапе выполнения программы, вывод типов не может знать результата его вычисления на этапе компиляции. Поэтому, чтобы не нарушить типобезопасность, он вынужден указывать объединенный тип, определяемый всеми блоками выражения.

Вывод типов кортеж (Tuple)

Начать стоит с напоминания о том, что значение длины кортежа содержащего элементы помеченные как не обязательные, принадлежит к типу объединению (Union), который составляют литеральные числовые типы.

function f(...rest: [number, string?, boolean?]): [number, string?, boolean?] {
  return rest
}

let l = f(5).length // let l: 1 | 2 | 3

Кроме того, остаточные параметры (...rest), аннотированные с помощью параметра типа, рассматриваются и представляются выводом типа, как принадлежащие к типу-кортежу.

function f<T extends any[]>(...rest: T): T {
  return rest
}

// рассматриваются

f(5) // function f<[number]>(rest_0: number): void
f(5, '') // function f<[number, string]>(rest_0: number, rest_1: string): void
f(5, '', true) // function f<[number, string, boolean]>(rest_0: number, rest_1: string, rest_2: boolean): void

// представляются

let v0 = f(5) // let v0: [number]
let v1 = f(5, '') // let v1: [number, string]
let v2 = f(5, '', true) // let v2: [number, string, boolean]

Если функция, сигнатура которой содержит объявление остаточных параметров, принадлежащих к типу параметра, будет вызвана с аргументами, среди которых будет массив, указанный с помощью расширяющего синтаксиса (spread syntax), то определение типа кортеж также будет включать в себя остаточный тип (...rest).

function tuple<T extends any[]>(...args: T): T {
  return args
}

let numberAll: number[] = [0, 1, 2]
let v0 = tuple(5, '', true) // let v0: [number, string, boolean]
let v1 = tuple(5, ...numberAll) // let v1: [number, ...number[]]

Но нужно быть внимательным, так как порядок передачи аргументов, содержащих расширяющий синтаксис, может привести к неочевидным результатам. Дело в том, что остаточный тип определяется не только типами самого массива, но и типами аргументов, переданных после него.

function tuple<T extends any[]>(...args: T): T {
  return args
}

let numberAll: number[] = [0, 1, 2]
let v0 = tuple(5, ...numberAll) // let v0: [number, ...number[]]
let v1 = tuple(5, ...numberAll, '') // let v1: [number, ...(string | number)[]]