Deriving Types From Types in TypeScript

How Types Are Deriving from Types in TypeScript?

One of the most powerful aspects of TypeScript is the system of creating types from other types (aka expressing types in terms of other types). The system includes:

  • generics,

  • produced literal key union types,

  • mapped types,

  • conditional types,

  • template literal types, and

  • index accessed types.

Generics

A generic is a type or an interface that is parametrized with one or more type parameters (aka type variables).

In TypeScript, it is possible to create a generic type alias or a generic interface but not a generic enum, nor generic namespace.

Generic Type Aliases

A generic type alias is a type alias that is parametrized with one or more type parameters.

type Items<Item> = {
  items: Item[]
}

type Car = {
  make: string,
  doors: number
}

type Cars = Items<Car>
// type Cars = { items: Car[] }

Generic Interfaces

A generic interface is an interface that is parametrized with one or more type parameters.

interface Items<Item> {
  items: Item[]
}

Generic Function Signatures

Generic function signatures are function signatures parametrized with one or more type parameters.

The below function allows a value to be pushed to an array only when the array already includes a value of the type as the value attempted to be pushed.

function pushToArray<T>(arr: T[], el: T): T {
  arr.push(el)

  return el
}

pushToArray([42], 23) // OK => 23
pushToArray([42], '23') // TS: Argument of type 'string' is not assignable to parameter of type 'number'.
pushToArray(['42'], '23') // OK => '23'
pushToArray(['42', 42], 23) // OK => 23
pushToArray(['42', 42], '23') // OK => '23'
pushToArray(['42', 42], {}) // TS: Argument of type '{}' is not assignable to parameter of type 'string | number'.

In the above example, the type of arr was inferred from the function call. But when an empty array would be pushed it would allow values of any type to be pushed. For cases such as this and for cases in which the compiler fails to infer types as intended it is possible to explicitly pass type to a generic function.

pushToArray<number>([], 23) // OK => 23
pushToArray<number>([], '23') // TS: Argument of type 'string' is not assignable to parameter of type 'number'.

The pushToArray function type can also be declared using arrow function type signatures, type aliases, and interfaces.

type PushToArray1 = <T>(arr: T[], el: T) => T

type PushToArray2 = {
  <T>(arr: T[], el: T): T
}

interface PushToArray3 {
  <T>(arr: T[], el: T): T
}

const pushToArray: PushToArray3 = (arr, el) => {
  arr.push(el)

  return el
}

Generic Classes

In addition to creating generic type aliases, generic interfaces, and generic function signatures, TypeScript allows for creating generic classes.

class Horse<Rider> {
  name: string
  rider: Rider
}

type Witcher = {
  name: string,
  school: string
}

type King = {
  name: string,
  kingdom: string
}

const geralt: Witcher = { name: 'Geralt', school: 'School of the Wolf' }
const alexander: King = { name: 'Alexander', kingdom: 'Macedon' }
const geraltHorse = new Horse<Witcher>()
geraltHorse.name = 'Roach'
geraltHorse.rider = geralt // OK.
geraltHorse.rider = alexander // TS: Property 'school' is missing in type 'King' but required in type 'Witcher'.

In the above example, a respective type is being passed to the Horse class at instance initialization syntactic indication (pre-runtime). It can also be done at class extension.

class WitcherHorse extends Horse<Witcher> {
  name: string
  rider: Witcher
}

One of the crucial things to remember about generic classes is that generic type parameters can only parametrize instance members and not static members.

Generic Constraints

It is possible to constraint the structural shape of types acceptable by function variable type parameters using the extends keyword.

type Swordsman = {
  name: string,
  swordsmanshipSkills: number
}

function logWitcher<Witcher extends Swordsman>(witcher: Witcher): void {
  console.log(witcher)
}

logWitcher({ name: 'Geralt', swordsmanshipSkills: 10 })
// {name: 'Geralt', hasSwordsmanshipSkills: true}

logWitcher({ name: 'Yennefer', magicSkills: 10 })
// TS: Argument of type '{ name: string; magicSkills: number; }' is not assignable to parameter of type 'Swordsman'.
// TS: Object literal may only specify known properties, and 'magicSkills' does not exist in type 'Swordsman'.

logWitcher({ name: 'Joe' })
// TS: Argument of type '{ name: string; }' is not assignable to parameter of type 'Swordsman'.
// TS: Property 'swordsmanshipSkills' is missing in type '{ name: string; }' but required in type 'Swordsman'.

Produced Literal Key Union Types

It is possible to create a new type being the literal union of keys of another type using the keyof operator.

type Captain = {
  name: string,
  ship: string,
  hasCaptainingSkills: true
}

type CaptainKeys = keyof Captain
// 'name' | 'ship' | 'hasCaptainingSkills'

const ship: CaptainKeys = 'ship' // OK
const anotherShip: CaptainKeys = 'plane' // Type '"plane"' is not assignable to type 'keyof Captain'.

Using the keyof operator on a type that has a string index signature produces the string | number union type.

type Foo = {
  [k: string]: any
}

type Bar = keyof Foo

let baz: Bar = 42 // OK
baz = '42' // OK
baz = {} // Type '{}' is not assignable to type 'string | number'.

Using the keyof operator on a type that has a number for an index signature produces the number type.

type Foo = {
  [k: number]: any
}

type Bar = keyof Foo

let baz: Bar = 42 // OK
baz = '42' // Type 'string' is not assignable to type 'number'.
baz = {} // Type '{}' is not assignable to type 'string | number'.

Mapped Types

What is a mapped type?

A mapped type is a structural type created through mapping over a union of keys of another structural type. Such a union is often created using the keyof operator. Mapped types are created using the index signature syntax.

Changing Property Types

A mapped type can be used to change a structural type property types.

type Numbered = {
  foo: number,
  bar: number,
  baz: number
}

type Stringified<T> = {
  [P in keyof T]: string
}

const qux: Numbered = { foo: 1, bar: 2, baz: 3 } // OK
const quux: Numbered = { foo: '1', bar: '2', baz: '3' } // TS: Type 'string' is not assignable to type 'number'.
const corge: Stringified<Numbered> = { foo: '1', bar: '2', baz: '3' } // OK

Changing Property Keys

A mapped type can be used to change (remap) keys of a structural type using the as operator combined with template literal types or other strategies.

type Numbered = {
  foo: number,
  bar: number,
  baz: number
}

type Underscored<T> = {
  [P in keyof T as `_${string & P}`]: T[P]
}

const qux: Underscored<Numbered> = { _foo: 1, _bar: 2, _baz: 3 } // OK
const quux: Underscored<Numbered> = { foo: 1, bar: 2, baz: 3 }
// TS: Type '{ foo: number; bar: number; baz: number; }' is not assignable to type 'Underscored<Numbered>'.
// TS: Object literal may only specify known properties, but 'foo' does not exist in type 'Underscored<Numbered>'. Did you mean to write '_foo'?

It is possible to change both property keys and property types simultaneously.

It is also possible to change the mutability and optionality modifiers through using the readonly and ? operators appended with - or the default +.

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}

type Immutable<T> = {
  +readonly [P in keyof T]: T[P]
}

type Optional<T> = {
  [P in keyof T]+?: T[P]
}

type Required<T> = {
  [P in keyof T]-?: T[P]
}

Removing Selected Properties

A mapped type can be used to remove (filter out) selected properties through producing never type for a given property during mapping.

type Numbered = {
  foo: number,
  bar: number,
  baz: number
}

type Bazless<T> = {
  [P in keyof T as ('foo' | 'bar') & P]: T[P]
}

const qux: Bazless<Numbered> = { foo: 1, bar: 2 } // OK
const quxx: Bazless<Numbered> = { foo: 1, bar: 2, baz: 3 }
// Type '{ foo: number; bar: number; baz: number; }' is not assignable to type 'Bazless<Numbered>'.
// Object literal may only specify known properties, and 'baz' does not exist in type 'Bazless<Numbered>'.

Conditional Types

A conditional type is a type which depends on an assignability condition between two types. A conditional type condition mimics the JavaScript ternary operator and uses the extends keyword: type Foo = Bar extends Baz ? Qux : Quux.

type Captain = {
  name: string
}

type CaptainHook = {
  name: 'Hook',
  ship: 'Jolly Roger'
}

type Hookable = CaptainHook extends Captain ? CaptainHook : never
const hook: Hookable = { name: 'Hook', ship: 'Jolly Roger' } // OK
const witcher: Hookable = { name: 'Geralt' }
// Type '"Geralt"' is not assignable to type '"Hook"'.
// The expected type comes from property 'name' which is declared here on type 'CaptainHook'

In the above example, as CaptainHook is assignable to Captain the type Hookable is CaptainHook.

Conditional types can be used with generics.

type Captainable = {
  name: string,
  ship: string
}

type Shipable<T> = T extends { ship: string } ? T : never
type CaptainHook = Shipable<Captainable>
const hook: CaptainHook = { name: 'Hook', ship: 'Jolly Roger' }
const witcher: CaptainHook = { name: 'Geralt' } // TS: Property 'ship' is missing in type '{ name: string; }' but required in type 'Captainable'.

Template Literal Types

Template literal types are types utilizing string literal types and interpolation to create new string literal types.

type Hook = 'Hook'
type CaptainHook = `Captain ${Hook}`
const captainHook: CaptainHook = 'Captain Hook'
const captainBlackbeard: CaptainHook = 'Captain Blackbeard' // TS: Type '"Captain Blackbeard"' is not assignable to type '"Captain Hook"'.

The power of template literal types is their ability to create union types through a combination of interpolation and string literal types.

type Captains = 'Hook' | 'Blackbeard' | 'Picard'
type Captain = `Captain ${Captains}`
const hook: Captain = 'Captain Hook' // OK
const blackbeard: Captain = 'Captain Blackbeard' // OK
const tuvok: Captain = 'Captain Tuvok' // TS: Type '"Captain Tuvok"' is not assignable to type '"Captain Hook" | "Captain Blackbeard" | "Captain Picard"'. Did you mean '"Captain Hook"'?

Further, when more than one string literal type is interpolated in one template literal string they create a cartesian product of all possible combinations.

type Letters = 'A' | 'B' | 'C'
type Numbers = '1' | '2' | '3'
type LettersAndNumbers = `${Letters}${Numbers}`
const foo: LettersAndNumbers = 'A1' // OK
const bar: LettersAndNumbers = 'C2' // OK
const baz: LettersAndNumbers = 'D4' // TS: Type '"D4"' is not assignable to type '"A1" | "A2" | "A3" | "B1" | "B2" | "B3" | "C1" | "C2" | "C3"'.

As presented in earlier chapters template literal types can be used with generics.

Indexed Access Types

An indexed access type is a type created from a property of another type of structural nature.

The index access type syntax uses the square bracket notation to access the relevant property of another type.

type People = {
  captains: { name: string, ship: string },
  pilots: { name: string, plane: string }
}

type Captain = People['captains']
const hook: Captain = { name: 'Hook', ship: 'Jolly Roger' } // OK
const ameliaEarhart: Captain = { name: 'Amelia Earhart', plane: 'Lockheed Vega 5B' }

// TS: Type '{ name: string; plane: string; }' is not assignable to type '{ name: string; ship: string; }'.
// TS: Object literal may only specify known properties, and 'plane' does not exist in type '{ name: string; ship: string; }'.

We use cookies and similar technologies to enhance the quality of services, maintain statistics and adjust marketing content. You will find more information in the Cookies Policy.

By clicking OK you grant consent to processing of your personal data by us and our Trusted Partners with the purpose of maintain statistics and adjustment of the marketing content pursuant to the Privacy Policy. If you wish to not grant that consent and/or limit its extent click Settings.