阿里云主机折上折
  • 微信号
Current Site:Index > Comparison between the unknown type and the any type

Comparison between the unknown type and the any type

Author:Chuan Chen 阅读数:56198人阅读 分类: TypeScript

Basic Concepts of unknown and any Types

In TypeScript, both unknown and any are top-level types, but they behave very differently. The any type allows bypassing type checking, while the unknown type enforces type checking. These two types serve different purposes when handling uncertain data, but they differ significantly in type safety.

let anyValue: any = "hello";
let unknownValue: unknown = "world";

// any type can be directly operated on
anyValue.toUpperCase(); // no error

// unknown type requires type assertion or type checking
if (typeof unknownValue === "string") {
  unknownValue.toUpperCase(); // safe operation
}

Differences in Type Safety

The any type completely bypasses type checking, equivalent to returning to pure JavaScript development. In contrast, the unknown type requires developers to explicitly perform type checks or assertions before operating on values, providing better type safety.

function processAny(data: any) {
  data.methodThatMightNotExist(); // compiles, but may fail at runtime
}

function processUnknown(data: unknown) {
  // data.methodThatMightNotExist(); // compile error
  if (typeof data === "object" && data !== null && "methodThatMightNotExist" in data) {
    (data as { methodThatMightNotExist: () => void }).methodThatMightNotExist();
  }
}

Use Case Comparison

The any type is typically used in the following scenarios:

  • Temporary solutions when migrating old JavaScript code to TypeScript
  • Cases where the type system needs to be completely bypassed
  • Interacting with third-party libraries with uncertain type structures

The unknown type is more suitable for:

  • Handling uncertain data from external sources (e.g., API responses)
  • Implementing type-safe dynamic code
  • Writing stricter generic functions
// API response handling with any
function parseResponseAny(response: any) {
  return response.data.items.map((item: any) => item.name);
}

// API response handling with unknown
function parseResponseUnknown(response: unknown) {
  if (
    typeof response === "object" &&
    response !== null &&
    "data" in response &&
    typeof response.data === "object" &&
    response.data !== null &&
    "items" in response.data &&
    Array.isArray(response.data.items)
  ) {
    return response.data.items
      .filter((item): item is { name: string } => typeof item?.name === "string")
      .map((item) => item.name);
  }
  throw new Error("Invalid response structure");
}

Type Inference Behavior

When using the any type, TypeScript allows any operation without type inference. In contrast, the unknown type requires type narrowing to use the value.

let anyVar: any = "test";
let unknownVar: unknown = "test";

// any type "infects" the result
let anyResult = anyVar + 123; // type is any

// unknown type requires explicit handling
let unknownResult;
if (typeof unknownVar === "string") {
  unknownResult = unknownVar + 123; // type is string
} else {
  unknownResult = "default";
}

Interaction with Other Types

The unknown type can be assigned to any type (with type assertion or checking), while the any type can also be assigned to any type without checking. Conversely, any type can be assigned to unknown, but only any and unknown can be assigned to any.

let a: any;
let u: unknown;
let s: string = "hello";

a = s; // allowed
u = s; // allowed

s = a; // allowed
s = u; // requires type assertion or checking

s = u as string; // type assertion
if (typeof u === "string") s = u; // type checking

Usage in Function Return Values

In function return values, unknown and any also differ significantly. A function returning unknown forces the caller to handle type uncertainty, while a function returning any shifts the responsibility entirely to the caller.

function getAny(): any {
  return JSON.parse(localStorage.getItem("data") || "null");
}

function getUnknown(): unknown {
  return JSON.parse(localStorage.getItem("data") || "null");
}

// Using any return value
const anyData = getAny();
anyData.someProperty; // compiles but may fail at runtime

// Using unknown return value
const unknownData = getUnknown();
// unknownData.someProperty; // compile error
if (typeof unknownData === "object" && unknownData !== null && "someProperty" in unknownData) {
  console.log(unknownData.someProperty); // safe access
}

Different Impacts of Type Assertions

Type assertions on any are unnecessary since it can already be used as any type. In contrast, type assertions on unknown are a common way to convert it to a specific type.

const anyValue: any = "hello";
const unknownValue: unknown = "world";

// any type assertion is redundant but legal
const anyAsNumber: number = anyValue as number;

// unknown type assertion is necessary
const unknownAsString: string = unknownValue as string;

// Safer way to assert unknown type
function isString(value: unknown): value is string {
  return typeof value === "string";
}

if (isString(unknownValue)) {
  console.log(unknownValue.toUpperCase());
}

Application in Generic Programming

In generic programming, unknown is generally safer than any because it doesn't break the integrity of the type system.

// Unsafe generic cache using any
class CacheAny {
  private data: any;
  
  set<T>(value: T): void {
    this.data = value;
  }
  
  get<T>(): T {
    return this.data;
  }
}

// Safer generic cache using unknown
class CacheUnknown {
  private data: unknown;
  
  set<T>(value: T): void {
    this.data = value;
  }
  
  get<T>(): T | undefined {
    if (this.data !== undefined) {
      return this.data as T;
    }
    return undefined;
  }
}

const cacheAny = new CacheAny();
cacheAny.set<string>("test");
const num = cacheAny.get<number>(); // compiles but is type-unsafe

const cacheUnknown = new CacheUnknown();
cacheUnknown.set<string>("test");
// const num2 = cacheUnknown.get<number>(); // requires handling undefined case
const str = cacheUnknown.get<string>(); // safer

Performance Considerations

From a performance perspective, unknown and any have no runtime differences since TypeScript types exist only at compile time. However, in terms of development experience and code maintainability, unknown generally offers better long-term benefits.

// Quick prototyping with any
function quickAndDirty(input: any) {
  // quickly write logic
  return input * 2;
}

// Gradual type refinement with unknown
function typeSafe(input: unknown) {
  if (typeof input === "number") {
    return input * 2;
  }
  throw new Error("Invalid input type");
}

Interoperability with Third-Party Libraries

When interacting with third-party JavaScript libraries lacking type definitions, unknown is generally safer than any because it forces developers to explicitly handle uncertain types.

// Calling a third-party library with any
declare function legacyLibAny(): any;
const resultAny = legacyLibAny();
resultAny.doSomething(); // dangerous, no type checking

// Calling a third-party library with unknown
declare function legacyLibUnknown(): unknown;
const resultUnknown = legacyLibUnknown();
if (
  typeof resultUnknown === "object" &&
  resultUnknown !== null &&
  "doSomething" in resultUnknown &&
  typeof resultUnknown.doSomething === "function"
) {
  resultUnknown.doSomething(); // safe call
}

Application of Type Guards

The unknown type works best with type guards, allowing gradual narrowing of the type scope, while any doesn't require type guards.

// Type guard for complex unknown data structures
interface User {
  id: number;
  name: string;
  email?: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "number" &&
    "name" in value &&
    typeof value.name === "string" &&
    (!("email" in value) || typeof value.email === "string")
  );
}

function processUser(data: unknown) {
  if (isUser(data)) {
    console.log(`User: ${data.name}, ID: ${data.id}`);
    if (data.email) {
      console.log(`Email: ${data.email}`);
    }
  } else {
    console.error("Invalid user data");
  }
}

Usage in Error Handling

When handling code that may throw exceptions, unknown is a better choice because the error parameter in catch clauses is of type unknown by default in TypeScript 4.0+.

// In older TypeScript versions, error is of type any
try {
  // code that may throw
} catch (error: any) {
  console.log(error.message); // unsafe, error may not have a message property
}

// In TypeScript 4.0+, error is of type unknown
try {
  // code that may throw
} catch (error) {
  if (error instanceof Error) {
    console.log(error.message); // safe
  } else {
    console.log("Unknown error occurred");
  }
}

Behavior in Union Types

In union types, unknown absorbs all types (since any type can be assigned to unknown), while any combined with any type results in any.

type T1 = unknown | string; // unknown
type T2 = any | string; // any

function example(value: unknown | string) {
  // value remains unknown, requires type checking
  if (typeof value === "string") {
    console.log(value.length);
  }
}

function example2(value: any | string) {
  // value is any, can be directly operated on
  console.log(value.length); // unsafe
}

Behavior in Intersection Types

In intersection types, unknown is a neutral element (intersecting with any type yields the original type), while any intersecting with any type results in any.

type T3 = unknown & string; // string
type T4 = any & string; // any

interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person> & unknown; // Partial<Person>
type PartialPersonAny = Partial<Person> & any; // any

Behavior in Conditional Types

In conditional types, unknown behaves like a strict top-level type, while any breaks the distributive property of conditional types.

type IsString<T> = T extends string ? true : false;

type A = IsString<unknown>; // false
type B = IsString<any>; // boolean (true | false)

type FilterStrings<T> = T extends string ? T : never;
type C = FilterStrings<unknown | string | number>; // string
type D = FilterStrings<any | string | number>; // string | any

Relationship with the never Type

unknown and never are at opposite ends of the type system: unknown is the supertype of all types, while never is the subtype of all types. any, however, is both a supertype and a subtype.

declare let neverValue: never;
declare let unknownValue: unknown;
declare let anyValue: any;

neverValue = unknownValue; // error
unknownValue = neverValue; // allowed

neverValue = anyValue; // allowed
anyValue = neverValue; // allowed

Behavior in Mapped Types

When processing unknown and any with mapped types, their behaviors also differ significantly.

type MakeOptional<T> = {
  [K in keyof T]?: T[K];
};

type OptionalAny = MakeOptional<any>; // { [key: string]: any; }
type OptionalUnknown = MakeOptional<unknown>; // {}

// Handling unknown requires checking if it's an object type first
type SafeMakeOptional<T> = unknown extends T
  ? {}
  : T extends object
  ? {
      [K in keyof T]?: T[K];
    }
  : T;

type Test1 = SafeMakeOptional<any>; // {}
type Test2 = SafeMakeOptional<unknown>; // {}
type Test3 = SafeMakeOptional<{ a: number; b: string }>; // { a?: number; b?: string; }

Covariance and Contravariance in Function Parameters

In function parameter positions, unknown behaves contravariantly (can accept more specific types), while any breaks normal variance rules.

type Handler<T> = (arg: T) => void;

let unknownHandler: Handler<unknown>;
let anyHandler: Handler<any>;

// Can assign more specific handlers
unknownHandler = (arg: string) => console.log(arg.length); // allowed
anyHandler = (arg: string) => console.log(arg.length); // allowed

// But not the other way around
let specificHandler: Handler<string>;
specificHandler = unknownHandler; // error
specificHandler = anyHandler; // allowed but unsafe

Usage in Index Signatures

When dealing with dynamic property access, unknown provides better safety than any.

interface DynamicObject {
  [key: string]: unknown;
}

function processDynamicObject(obj: DynamicObject) {
  for (const key in obj) {
    const value = obj[key];
    if (typeof value === "string") {
      console.log(`String value at ${key}: ${value.toUpperCase()}`);
    } else if (typeof value === "number") {
      console.log(`Number value at ${key}: ${value.toFixed(2)}`);
    }
  }
}

// Comparison with any version
interface DynamicObjectAny {
  [key: string]: any;
}

function processDynamicObjectAny(obj: DynamicObjectAny) {
  for (const key in obj) {
    const value = obj[key];
    console.log(value.toUpperCase()); // may fail at runtime
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.