Leo Jiang
Leo Jiang

Reputation: 26085

TypeScript optional object key not behaving as expected

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

Answers (5)

hoangdv
hoangdv

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

Thiago Mata
Thiago Mata

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:

Making a For Loop Accepting Any Value

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

Making Explicit Attributes Set

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

Using the Nullable Cast

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

Joseph Garrone
Joseph Garrone

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

}

Playground Link

Slackblitz link.

NOTE: That Slackblitz is configured with typescript very permissive, you don't get all the warnings you get in the TS playgound.

Upvotes: 0

Shaun Luttin
Shaun Luttin

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

Ernesto
Ernesto

Reputation: 4252

$ npm i -D @types/node @types/express

just extend from express

export interface MyPrequest extends express.Resquest {}

Upvotes: -1

Related Questions