Reputation: 21730
Short version:
I have an external type ExternalFoo
that I can not modify.
I need to build type Foo
which is equivalent to the ExternalFoo
except that it should not accept string
.
type ExternalFoo = {} | number | string;
type Foo = ???;
const foo1: Foo = {}; // ok
const foo1: Foo = 5; // ok
const foo2: Foo = "bar"; // not ok
Is it possible to define Foo
based on the given ExternalFoo
to satisfy this condition?
Longer version
I want to build a parametarized type that can accept any given T
and return T1
that is identical to T
but doesn't allow for string.
I have a React app, and I want to force translation on it, we have a method forceTranslation
method that accepts any React element, and changes specified properties such that they no longer accept string
but can accept TranslatedString
.
So if you have a component <User firstName={"bob"}/>
the wrapped version (const TranslatedUser = forceTranslation(User, ["bob"]);
) will compile-time fail if you give it "bob"
but will succeed if you give it translate("bob")
.
The issue is that children
property of type React.ReactNode
include {}
in its definition. Using Exclude<T, {}>
will also exclude number
(among other things) from the type.
Upvotes: 1
Views: 2085
Reputation: 327704
The so-called "empty object type" {}
is very wide; almost all values will be assignable to it. Roughly, it will accept any value that can be indexed into like an object. This includes all non-primitive types like object
, but it also includes the five primitive types with wrapper objects: so string
, number
, boolean
, bigint
, and symbol
are assignable to {}
because values of these types are automatically wrapped in String
, Number
, Boolean
, BigInt
, and Symbol
objects (respectively) when you access members on them as if they were objects.
In some sense this means your ExternalFoo
type that cannot be changed is redundant: {} | string | number
is basically equivalent to {}
(but is both displayed differently and might be treated differently by the compiler in some circumstances). I'm wondering if the actual intent of ExternalFoo
was supposed to be more like object | string | number
instead of {} | string | number
. Was it really meant to accept boolean
, for example?
const wasThisIntended: ExternalFoo = true;
But you said you can't change it, so that's out of scope.
TypeScript does not have negated types (see microsoft/TypeScript#29317) so you cannot take ExternalFoo
and quickly produce the version of it that rejects string
. Such a type like ExternalFoo & not string
is simply not representable in TypeScript as it currently exists. The Exclude
utility type only filters unions. And while {} | number | string
can be filtered to produce either {} | number
, {}
itself cannot be filtered because it is not a union.
In order to exclude string
from ExternalFoo
, we would need not only to remove string
from the definition, but change {}
to something that also excludes string
. The type {}
is more or less the same as object | string | number | boolean | bigint | symbol
. We can, if we want, remove string
from that, to produce
type Foo = object | number | boolean | bigint | symbol;
So let's try that!
const foo1: Foo = {}; // okay
const foo2: Foo = 5; // okay
const foo3: Foo = "bar"; // error!
Looks good. As desired, you can assign {}
and 5
to a variable of type Foo
, but you cannot assign "bar"
to it. This still happens:
const stillHappens: Foo = true; // okay
But boolean
is not a string
so I guess it's desired also.
For your longer version, I'd suggest trying to convert a generic type T extends ExternalFoo
to T & Foo
and see if it works for you:
type HasLength = { length: number };
let l: HasLength;
l = [1, 2, 3]; // okay
l = "hello"; // okay
l = { length: 19 }; // okay
type NotString<T extends ExternalFoo> = T & Foo;
type HasLengthNotString = NotString<{ length: number }>;
let n: HasLengthNotString;
n = [1, 2, 3]; // okay
n = "hello"; // error
n = { length: 19 }; // okay
Upvotes: 2
Reputation: 1073
You can use Exclude
utility.
type ExternalFoo = number | string;
type Foo = Exclude<ExternalFoo, string>;
const a: Foo = "hi"; // Error: Type 'string' is not assignable to type 'number'.ts(2322)
All built-in utilities in TypeScript: https://www.typescriptlang.org/docs/handbook/utility-types.html
Upvotes: -2