TypeScript 4.7: New Features and Improvements
TypeScript 4.7: New Features and Improvements
TypeScript 4.7 brings significant improvements to module resolution, type inference, and developer experience. Let's explore the key features that make your TypeScript code safer and more expressive.
Module Detection Control
TypeScript 4.7 introduces the moduleDetection setting to control how TypeScript determines whether a file is a module.
The Problem
Before 4.7, TypeScript would treat any file with import or export as a module. Files without these were treated as scripts, which could cause issues.
The Solution
// tsconfig.json
{
"compilerOptions": {
// "force" - All files are treated as modules
// "legacy" - Old behavior (imports/exports determine module status)
// "auto" - Default, smart detection based on module setting
"moduleDetection": "force"
}
}
When to Use "force"
// script.ts - Without moduleDetection: "force", this causes errors
const greeting = "Hello"
export { greeting } // Now works in every file
// Useful for projects where every file should be a module
// Prevents global scope pollution
Control Flow Analysis for Computed Properties
TypeScript 4.7 improves narrowing for computed property access:
function getPropertyValue(obj: Record<string, unknown>, key: string) {
if (key in obj) {
// TypeScript now knows obj[key] is defined
return obj[key] // Type: unknown (correctly narrowed)
}
return undefined
}
// Example with object types
interface Config {
debug?: boolean
production?: boolean
}
function setConfig(config: Config, key: 'debug' | 'production') {
if (key in config && config[key] !== undefined) {
// Properly narrowed
console.log(`Setting ${key} to ${config[key]}`)
}
}
Improved Function Inference in Objects
TypeScript 4.7 improves inference for functions in object literals:
type Functions = {
func?: (value: string) => string
}
declare function register(obj: Functions): void
// Before 4.7: 'value' would be 'any'
register({
func(value) {
// Now correctly inferred as string
return value.toUpperCase()
}
})
// Works with methods too
type Methods = {
method?(value: number): boolean
}
register({
method(value) {
// Correctly inferred as number
return value > 0
}
})
Instantiation Expressions
A new syntax for specifying generic type arguments without calling the function:
class Box<T> {
value: T
constructor(value: T) {
this.value = value
}
}
// Create a type without calling the constructor
const NumberBox = Box<number>
type NumberBox = InstanceType<typeof NumberBox>
function makeBox<T>(value: T) {
return new Box(value)
}
// Specify generic without calling
const makeStringBox = makeBox<string>
type StringBox = ReturnType<typeof makeStringBox> // Box<string>
// Practical example with error handling
type ErrorHandler<E> = (error: E) => void
function createHandler<E>(handler: ErrorHandler<E>) {
return handler
}
const networkErrorHandler = createHandler<NetworkError>
extends Constraint on infer
TypeScript 4.7 allows adding constraints to infer type variables:
// Before: No way to constrain the inferred type
type FirstString<T> = T extends [infer S, ...unknown[]]
? S
: never
// Would accept any first element
type A = FirstString<[number, string]> // number (not desired)
// After 4.7: Can constrain the inferred type
type FirstStringConstrained<T> = T extends [infer S extends string, ...unknown[]]
? S
: never
type B = FirstStringConstrained<[number, string]> // never
type C = FirstStringConstrained<["hello", number]> // "hello"
// Practical use case
type ExtractStringProperty<T> = T extends { name: infer N extends string }
? N
: never
type Result = ExtractStringProperty<{ name: "Alice"; age: 30 }> // "Alice"
type Failed = ExtractStringProperty<{ name: 123; age: 30 }> // never
Optional Variance Annotations
TypeScript 4.7 adds explicit variance annotations for type parameters:
Understanding Variance
// Covariant: Used in output positions
type Getter<T> = () => T
// Contravariant: Used in input positions
type Setter<T> = (value: T) => void
// Invariant: Used in both
type Box<T> = {
get(): T
set(value: T): void
}
Explicit Annotations
// Covariant parameter
interface Getter<out T> {
get(): T
}
// Contravariant parameter
interface Setter<in T> {
set(value: T): void
}
// Invariant (both directions)
interface Box<in out T> {
get(): T
set(value: T): void
}
// Practical example with generic collections
interface Producer<out T> {
produce(): T
}
interface Consumer<in T> {
consume(item: T): void
}
// TypeScript can now verify variance at definition time
interface Producer<out T> {
produce(): T
bad(item: T): void // Error! Contravariant use of out param
}
Resolution Customization with moduleSuffixes
For projects with platform-specific modules:
// tsconfig.json
{
"compilerOptions": {
"moduleSuffixes": [".browser", ".native", ""]
}
}
Usage
src/
├── utils.browser.ts
├── utils.native.ts
└── utils.ts
// In code
import { helper } from './utils'
// TypeScript will look for:
// 1. ./utils.browser.ts
// 2. ./utils.native.ts
// 3. ./utils.ts
// On web: resolves to utils.browser.ts
// On native: resolves to utils.native.ts
// Default: resolves to utils.ts
Resolution Customization for package.json Exports
TypeScript 4.7 supports package.json exports field for better module resolution:
// package.json
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
},
"./utils": {
"import": "./dist/esm/utils.js",
"require": "./dist/cjs/utils.js"
}
}
}
Consuming Package
// TypeScript resolves based on module setting
import { something } from 'my-package'
import { utility } from 'my-package/utils'
node16 and nodenext Module Resolution
New module resolution modes for modern Node.js:
// tsconfig.json
{
"compilerOptions": {
"module": "node16",
"moduleResolution": "node16"
}
}
Benefits
- Supports ESM and CJS interop
- Respects
package.jsonexports - Proper
.mjsand.cjshandling - File extension required for ESM imports
// With node16 resolution
import { something } from './module.js' // Extension required for ESM
import { other } from './commonjs.cjs' // CJS module
Object Method Syntax Improvements
interface Obj {
property: string
method(): void
asyncMethod?(): Promise<void>
}
const obj: Obj = {
property: "value",
method() {
// Correct 'this' typing
console.log(this.property)
},
async asyncMethod() {
// Async method syntax
}
}
Improved Type Inference for Array Methods
// Better inference for array methods
const items = [
{ type: 'a', value: 1 },
{ type: 'b', value: 'two' }
] as const
// TypeScript 4.7 correctly infers
type ItemType = typeof items[number]
// { readonly type: "a"; readonly value: 1 } | { readonly type: "b"; readonly value: "two" }
// Filter with type narrowing
const aItems = items.filter(item => item.type === 'a')
// Type is correctly narrowed to a-type items
Stricter Spread Checks
interface Song {
name: string
artist: string
}
interface Album {
name: string
artist: string
tracks: Song[]
}
const song: Song = { name: "Song", artist: "Artist" }
// Before 4.7: This would work (incorrectly)
const album: Album = {
...song,
tracks: []
}
// 4.7: Better checking - catches potential issues
interface Strict {
required: string
optional?: number
}
// Spreading from wider to narrower
declare let source: { required: string }
let target: Strict = { ...source } // Error: optional missing
Performance Improvements
TypeScript 4.7 includes significant performance improvements:
- Faster type checking - Up to 20% faster in some projects
- Reduced memory usage - Better for large codebases
- Faster incremental builds - Improved caching
New --assumeChangesOnlyAffectDirectDependencies Flag
// tsconfig.json
{
"compilerOptions": {
"assumeChangesOnlyAffectDirectDependencies": true
}
}
Speeds up incremental builds by assuming file changes only affect direct dependencies. Use with caution in complex projects.
Editor Improvements
Organize Imports Improvements
// Before organizing
import { useState, useEffect, useCallback, useMemo } from 'react'
import { Something } from 'unused-module'
// After organizing (removes unused, sorts)
import { useCallback, useEffect, useMemo, useState } from 'react'
Go-to-Definition Improvements
- Better navigation in composite projects
- Improved handling of re-exports
Breaking Changes
Stricter Checks for Spread Operations
// This pattern now may produce errors
interface Base {
foo: string
}
interface Extended extends Base {
bar: number
}
const base: Base = { foo: 'hello' }
const extended: Extended = { ...base, bar: 1 }
// Warning: Spread may lose optional properties
in Operator Stricter
// Before 4.7
"toString" in {} // true (runtime)
// TypeScript allowed this
// 4.7 is stricter
const obj = {} as Record<string, unknown>
if ("toString" in obj) {
// Still works for Record types
}
Migration Guide
- Update TypeScript:
npm install typescript@4.7
- Update tsconfig.json:
{
"compilerOptions": {
"target": "es2020",
"module": "node16",
"moduleResolution": "node16",
"moduleDetection": "force"
}
}
- Fix any new errors:
// Add file extensions for ESM imports
import { something } from './module.js'
// Use explicit variance annotations if needed
interface Producer<out T> {
produce(): T
}
Conclusion
TypeScript 4.7 brings meaningful improvements to the language, especially for module resolution and type system expressiveness. The new variance annotations and infer constraints enable more precise type definitions.
Key Takeaways
- Use
moduleDetection: "force"for safer module handling - Leverage
extendsconstraint oninferfor better type narrowing - Consider explicit variance annotations for complex generic types
- Adopt
node16module resolution for modern Node.js projects
Upgrade to TypeScript 4.7 to take advantage of these improvements!