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
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
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));
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
(...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));
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());
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.