Reputation: 26085
I have an object with conditional keys. I.e.:
const headers: RequestHeaders = {};
if (...) {
headers.foo = 'foo';
}
if (...) {
headers.bar = 'bar';
}
I'm new to TS and I expected this to work:
type RequestHeaders = {
foo?: string,
bar?: string,
};
However, I'm passing this to fetch
and the type definition for fetch
's headers is { [key: string]: string }
. I'm getting:
Type 'RequestHeaders' is not assignable to type '{ [key: string]: string; }'.
Property 'foo' is incompatible with index signature.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
The only way I could get this to work is type RequestHeaders = { [key: string]: string };
. Is there a way to limit the keys to a set of predefined strings?
Upvotes: 6
Views: 1295
Reputation: 16127
The header data of fetch
method must be type like
type HeadersInit = Headers | string[][] | Record<string, string>;
For your case, I think you will define headers
type as a alias of Record<string, string>
. For keys configuration (foo
, bar
), I will have a suggestion: Fixed header keys. Then, you will define all header keys to a type like:
type HeaderKeys = 'foo' | 'bar';
type RequestHeaders = Record<HeaderKeys, string>; // the same: type RequestHeaders = Record<'foo' | 'bar', string>;
const headers: RequestHeaders = {} as RequestHeaders; // force cast
Upvotes: 1
Reputation: 2959
So far I can say, If you want to move from well-defined optional attributes to flexible but required key-value attributes, you need to cast by hand. That could be made using strategies:
type RequestHeaders = {
foo?: string,
bar?: string,
};
type FetchResult = {
[key: string]: string;
}
let headers: RequestHeaders = {};
const i = 2
if (i > 1) {
headers.foo = 'foo-value';
}
if (i > 0) {
headers.bar = 'bar-value';
}
const fetchResult: FetchResult = {};
for (let key in headers) {
let value = headers[key] // <==== This will be of the type Any
if (value) {
fetchResult[key] = "" + value;
}
}
console.log(fetchResult);
type RequestHeaders = {
foo?: string,
bar?: string,
};
type FetchResult = {
[key: string]: string;
}
let headers: RequestHeaders = {};
const i = 2
if (i > 1) {
headers.foo = 'foo-value';
}
if (i > 0) {
headers.bar = 'bar-value';
}
const fetchResult: FetchResult = {};
if (headers.foo) {
fetchResult['foo'] = headers.foo;
}
if (headers.bar) {
fetchResult['bar'] = headers.bar;
}
console.log(fetchResult);
Similar to the @ShaunLutin example, using the " as " command allows you to check the result of the cast.
type RequestHeaders = {
foo?: string,
bar?: string,
};
type FetchResult = {
[key: string]: string;
}
let headers: RequestHeaders = {};
const i = 2
if (i > 1) {
headers.foo = 'foo-value';
}
if (i > 0) {
headers.bar = 'bar-value';
}
let fetchResultNullable: FetchResult | undefined;
fetchResultNullable = headers as FetchResult;
console.log(fetchResultNullable);
You can see those examples in this Typescript Playground
Upvotes: 0
Reputation: 1762
The TypeScript compiler is protecting you.
RequestHeaders
is not a valid Record<string,string>
it is right.
It's a good thing that it does not let you pass RequestHeaders
to fetch()
as depending on the implementation of fetch() some RequestHeaders
could make it throw.
/**
* The question mark means that the key
* can be missing
* OR
* The key is present and the value is a string
* OR
* The key is present and the value is undefined
*/
type RequestHeaders = {
foo?: string,
bar?: string,
};
/*
{ [k in 'foo' | 'bar']: string };
<=>
{ foo: string; bar: string; };
It's not a workaround, it's not the same type as RequestHeaders
and the example you gave doesn't transpile if you use it in place
of RequestHeaders.
*/
/**
* This is an implementation of fetch that throw
* if you pass it an object that have properties
* with undefined value
*/
function fetch(requestHeaders: { [key: string]: string; }): string{
return Object.keys(requestHeaders)
.map(key => `${key} => ${requestHeaders[key].toUpperCase()}`)
.join(" --- ")
;
}
/**
* This is an implementation of the function that transform a
* record with values that can be undefined to
* a record where if there is a key the value
* is a string.
*/
const toStringRecord= (
partialStringRecord: { [key: string]: string | undefined; }
): { [key: string]: string; }=> {
const out: { [key: string]: string; } = {};
Object.keys(partialStringRecord)
.filter(key=> typeof partialStringRecord[key] === "string")
.forEach(key=> out[key] = partialStringRecord[key]!)
;
return out;
}
const headers: RequestHeaders = {};
const cond1= true;
const cond2= false;
{
const requestHeaders: RequestHeaders= {};
//In your example you dont assign if cond is false
//but you could verry well wrote things this way and
//it would be perfectly valid typewise.
requestHeaders.foo = cond1 ? "foo" : undefined;
requestHeaders.bar = cond2 ? "bar" : undefined;
try{
//It's good that the compiler does not let you pass this to fetch
//as this throw.
fetch(requestHeaders as any);
}catch(error){
console.log(error.message);
}
//Works as expected, str === "foo => FOO"
const str= fetch(
toStringRecord(requestHeaders)
);
console.log(str);
}
NOTE: That Slackblitz is configured with typescript very permissive, you don't get all the warnings you get in the TS playgound.
Upvotes: 0
Reputation: 141472
The fetch API does not accept a headers object that has a key with an undefined value. Since each of your optional types can be either string | undefined
, the compiler is rejecting them.
Here is an approach that filters the headers to remove those with undefined values. Its type predicate (is
) keeps the compiler happy.
const buildHeaders = (requestHeaders: RequestHeaders): HeadersInit =>
Object.entries(requestHeaders).filter(
(entry): entry is [string, string] => entry[1] !== undefined
);
const headers: RequestHeaders = {};
type RequestHeaders = {
foo?: string; // optional
bar?: string; // optional
baz: string; // required!
};
fetch("Some Data", {
headers: buildHeaders(headers)
});
The advantage of this approach is that it lets you limit the keys to a set of predefined strings while also letting you to specify whether each is required or optional.
Upvotes: 2
Reputation: 4252
$ npm i -D @types/node @types/express
just extend from express
export interface MyPrequest extends express.Resquest {}
Upvotes: -1