Home > Software design >  "...args: never[]" vs "...args: unknown[]" in TypeScript
"...args: never[]" vs "...args: unknown[]" in TypeScript

Time:01-21

New to TypeScript here. I'm trying to define an interface for "things that have a .toString() method", where the .toString() can accept any number of arguments, of any type.

The following code is a minimum example, and using ...args: never[] as the arguments to toString() in the interface seems to work correctly. My question is why; can someone explain to me what the difference between unknown and never is in this case?

interface Stringable {
  // toString: (...args: any[]) => string; // works but type-unsafe
  // toString: (...args: unknown[]) => string; // causes num and object to error
  toString: (...args: never[]) => string; // seems to work
}

const num: Stringable = 4;
const word: Stringable = "hello";
const obj: Stringable = { toString: (text: string) => text   "!!" };

console.log(num.toString());
console.log(word.toString());
console.log(obj.toString("world"));

CodePudding user response:

never is the type you can never assign anything to but you can always assign from

let x: never = 1; // can't assign annything to
let o: string = x; // can assign from 

Playground Link

unknown is the opposite, it is the type you can always assign to, but you can never assign from

let x: unknown = 1; // can assign anything to
let o: string = x; // can't assign from 

Playground Link

In the context of a function signature (...args: never[]) => string will be a signature that can never be invoke with any arguments (since we can never assign anything to an parameter of type never). The consequence of this, is that we can assign any function implementation to such a signature, because if we pass in an argument of type never, since never is assignable type, that never value is assignable to a value of that parameter.

interface Stringable {
  toString: (...args: never[]) => string; 
}

// Since this is not a error
const obj: Stringable = { toString: (text: string) => text.toUpperCase()   "!!" };

// This has to be a TS error
console.log(obj.toString(1));

Playground Link

An unfortunate consequence is that while we can't pass in arguments, we can still call the function with no arguments. This seems like an unsoundness in the type system.

interface Stringable {
  toString: (...args: never[]) => string; 
}

const obj: Stringable = { toString: (text: string) => text.toLowerCase()   "!!" };

console.log(obj.toString("X")); // ts error
console.log(obj.toString()); // callable, will throw a runtime error

Playground Link

(...args: unknown[]) => string is a function signature that can take any number argument, with any values. This means that an implementation must be able to handle any values passed in. This means we can assign a function that does not handle it's arguments at all (such as String.toString(): string) but we can't assign one that needs specific types for any of it's arguments (such as Number.toString(radix?: number | undefined): string, or obj.toString: (text: string) => string)

Intuitively we ca see we want an error:

interface Stringable {
  toString: (...args: unknown[]) => string; 
}

// IF this is not an error
const obj: Stringable = { toString: (text: string) => text.toUpperCase()   "!!" };

//  this is a runtime error
console.log(obj.toString(1));

Playground Link

We can't in principle create a function signature that can hold safely all the following signatures: Number.toString(radix?: number | undefined): string, obj.toString: (text: string) => string, String.toString(): string. We can't do this because Number.toString and obj.toString expect conflicting types as the first argument. The only safe way to call both would be with an argument that satisfies BOTH signatures, which would be string and number which is string & number which is never.

The only toString we can safely represent one without parameters:

interface Stringable {
  toString: () => string; 
}

const num: Stringable = 4;
const word: Stringable = "hello";
const obj: Stringable = { toString: (text?: string) => text   "!!" };

console.log(num.toString());
console.log(word.toString());
console.log(obj.toString());

Playground Link

CodePudding user response:

unknown is basically polite (safer) any. You use it when type isn't known.

never on the other hand is something that never occurs - function without return value returns never. Function which has never[] for spread argument takes no parameter.

Error occurs when you try to call the function. In your example:

interface Stringable {
  toString: (...args: never[]) => string;
}

const obj: Stringable = { toString: (text: string) => text   "!!" };

console.log(obj.toString("world")); // -> Argument of type 'string' is not assignable to parameter of type 'never'.

For you usecase I think using generic type is best:

interface Stringable<T> {
  toString: (...args: T[]) => string;
}

const obj: Stringable<string> = { toString: (text) => text   "!!" };

console.log(obj.toString("world"));

There is nice comparision table between types in documentation.

  • Related